From bd50ee2a58371e46a6cebd6b2d764f544320cb03 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov Date: Sun, 9 Aug 2026 13:52:02 +0700 Subject: [PATCH 01/44] =?UTF-8?q?feat(booping):=20scaffold-seeded=20plan?= =?UTF-8?q?=20creation=20=E2=80=94=20unified-diff=20scaffold=20receipt=20a?= =?UTF-8?q?nd=20per-file=20write=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/booping/commands/scaffold.py | 34 +++--- booping-python/src/booping/utils.py | 21 ++++ .../tests/commands/scaffold_test.py | 103 ++++++++++++++++-- 3 files changed, 135 insertions(+), 23 deletions(-) diff --git a/booping-python/src/booping/commands/scaffold.py b/booping-python/src/booping/commands/scaffold.py index 7a3f44c7..761b5bc3 100644 --- a/booping-python/src/booping/commands/scaffold.py +++ b/booping-python/src/booping/commands/scaffold.py @@ -13,7 +13,7 @@ from booping.context.scaffold import DirNode, FileNode, ScaffoldError, load from booping.macros import parse_stub_overrides from booping.rendering import build_source_env -from booping.utils import deep_merge, parse_set_overrides +from booping.utils import deep_merge, diff_report, parse_set_overrides def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: # type: ignore[type-arg] @@ -38,7 +38,7 @@ def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) "--force", action="store_true", help=( - "Write into a non-empty destination, overwriting the files the tree names;" + "Overwrite the files the tree names when they already exist;" " never deletes a directory" ), ) @@ -104,7 +104,7 @@ def walk(node: DirNode, base: Path) -> None: return writes -def _apply(writes: list[_Write]) -> tuple[int, int, int]: +def _apply(writes: list[_Write], force: bool) -> tuple[int, int, int]: created_dirs = created_files = overwritten = 0 for write in writes: existed = write.path.exists() @@ -113,14 +113,24 @@ def _apply(writes: list[_Write]) -> tuple[int, int, int]: if not existed: print(f"created dir {write.path}") created_dirs += 1 + continue + + if existed and not force: + print(f"skipped existing file {write.path}") + continue + + previous = write.path.read_text(encoding="utf-8") if existed else None + if previous == write.content: + continue + + write.path.write_text(write.content, encoding="utf-8") + diff = diff_report(write.path, previous, write.content) + if diff: + print(diff) + if existed: + overwritten += 1 else: - write.path.write_text(write.content, encoding="utf-8") - if existed: - print(f"overwrote file {write.path}") - overwritten += 1 - else: - print(f"created file {write.path}") - created_files += 1 + created_files += 1 return created_dirs, created_files, overwritten @@ -148,8 +158,6 @@ def _run(args: argparse.Namespace) -> None: if dest.exists() and not dest.is_dir(): _fail(f"destination {dest} exists and is not a directory") - if dest.is_dir() and any(dest.iterdir()) and not args.force: - _fail(f"destination {dest} is not empty — pass --force to write into it") config = deep_merge(ctx.config, stub_overrides) if stub_overrides else ctx.config env = build_source_env(context=ctx, config=config) @@ -162,7 +170,7 @@ def _run(args: argparse.Namespace) -> None: _fail(str(exc)) try: - created_dirs, created_files, overwritten = _apply(writes) + created_dirs, created_files, overwritten = _apply(writes, args.force) except OSError as exc: _fail(f"failed to write scaffold into {dest}: {exc}", code=2) diff --git a/booping-python/src/booping/utils.py b/booping-python/src/booping/utils.py index 4b27bf4d..728f7aa6 100644 --- a/booping-python/src/booping/utils.py +++ b/booping-python/src/booping/utils.py @@ -1,7 +1,28 @@ +import difflib from collections.abc import Sequence +from pathlib import Path from typing import Any, cast +def diff_report(path: Path, previous: str | None, current: str) -> str: + """Unified diff of *previous* against *current* for *path*, empty when identical. + + ``previous`` is ``None`` when the file did not exist, which renders the + from-file as ``/dev/null``. + """ + if previous == current: + return "" + before = previous or "" + lines = difflib.unified_diff( + before.splitlines(), + current.splitlines(), + fromfile="/dev/null" if previous is None else str(path), + tofile=str(path), + lineterm="", + ) + return "\n".join(lines) + + class PathError(Exception): """Raised when a dotted path does not resolve in a mapping. diff --git a/booping-python/tests/commands/scaffold_test.py b/booping-python/tests/commands/scaffold_test.py index 50efd3d5..00017e44 100644 --- a/booping-python/tests/commands/scaffold_test.py +++ b/booping-python/tests/commands/scaffold_test.py @@ -76,16 +76,29 @@ def test_existing_empty_destination_written_without_force( assert (dest / "README.md").exists() -def test_non_empty_destination_without_force_exits_1_writing_nothing( +def test_non_empty_destination_without_force_fills_the_gaps( tmp_path: Path, isolated_xdg_config_home: Path ) -> None: dest = tmp_path / "out" dest.mkdir() (dest / "keep.txt").write_text("keep\n") result = _scaffold(tmp_path, isolated_xdg_config_home, "demo", str(dest)) - assert result.returncode == 1 - assert str(dest) in result.stderr - assert sorted(p.name for p in dest.iterdir()) == ["keep.txt"] + assert result.returncode == 0, result.stderr + assert (dest / "README.md").read_text() == "hello \n" + assert (dest / "keep.txt").read_text() == "keep\n" + + +def test_existing_file_without_force_is_skipped_and_reported( + tmp_path: Path, isolated_xdg_config_home: Path +) -> None: + dest = tmp_path / "out" + dest.mkdir() + (dest / "README.md").write_text("old\n") + result = _scaffold(tmp_path, isolated_xdg_config_home, "demo", str(dest)) + assert result.returncode == 0, result.stderr + assert (dest / "README.md").read_text() == "old\n" + assert f"skipped existing file {dest / 'README.md'}" in result.stdout.splitlines() + assert f"+++ {dest / 'README.md'}" not in result.stdout def test_force_overwrites_named_files_and_leaves_others( @@ -196,7 +209,7 @@ def test_malformed_set_pair_exits_1_matching_render_message( # --- Task 2.3: report, exit codes, logging --------------------------------- -def test_report_lines_and_summary( +def test_report_diffs_dirs_and_summary( tmp_path: Path, isolated_xdg_config_home: Path ) -> None: dest = tmp_path / "out" @@ -205,8 +218,13 @@ def test_report_lines_and_summary( result = _scaffold(tmp_path, isolated_xdg_config_home, "demo", str(dest), "--force") assert result.returncode == 0, result.stderr lines = result.stdout.splitlines() - assert f"overwrote file {dest / 'README.md'}" in lines - assert f"created file {dest / 'src' / 'main.py'}" in lines + assert f"--- {dest / 'README.md'}" in lines + assert f"+++ {dest / 'README.md'}" in lines + assert "-old" in lines + assert "+hello " in lines + assert "--- /dev/null" in lines + assert f"+++ {dest / 'src' / 'main.py'}" in lines + assert "+print(1)" in lines assert f"created dir {dest / '_references'}" in lines assert lines[-1] == ( "scaffolded 4 paths — 2 dirs created, 1 files created, 1 files overwritten" @@ -214,6 +232,68 @@ def test_report_lines_and_summary( assert result.stderr == "" +def test_new_file_diff_is_all_additions_from_dev_null( + tmp_path: Path, isolated_xdg_config_home: Path +) -> None: + dest = tmp_path / "out" + dest.mkdir() + result = _scaffold( + tmp_path, + isolated_xdg_config_home, + "demo", + str(dest), + tree='demo:\n a.txt: "one\\ntwo\\n"\n', + ) + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert lines[0] == "--- /dev/null" + assert lines[1] == f"+++ {dest / 'a.txt'}" + assert lines[2] == "@@ -0,0 +1,2 @@" + assert lines[3:5] == ["+one", "+two"] + + +def test_overwrite_diff_carries_context_lines( + tmp_path: Path, isolated_xdg_config_home: Path +) -> None: + dest = tmp_path / "out" + dest.mkdir() + (dest / "a.txt").write_text("one\nold\nthree\n") + result = _scaffold( + tmp_path, + isolated_xdg_config_home, + "demo", + str(dest), + "--force", + tree='demo:\n a.txt: "one\\ntwo\\nthree\\n"\n', + ) + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert lines[0] == f"--- {dest / 'a.txt'}" + assert lines[1] == f"+++ {dest / 'a.txt'}" + assert lines[2] == "@@ -1,3 +1,3 @@" + assert lines[3:7] == [" one", "-old", "+two", " three"] + + +def test_unchanged_file_prints_no_diff( + tmp_path: Path, isolated_xdg_config_home: Path +) -> None: + dest = tmp_path / "out" + dest.mkdir() + (dest / "a.txt").write_text("same\n") + result = _scaffold( + tmp_path, + isolated_xdg_config_home, + "demo", + str(dest), + "--force", + tree='demo:\n a.txt: "same\\n"\n', + ) + assert result.returncode == 0, result.stderr + assert result.stdout.splitlines() == [ + "scaffolded 0 paths — 0 dirs created, 0 files created, 0 files overwritten" + ] + + def test_unknown_config_path_exits_1( tmp_path: Path, isolated_xdg_config_home: Path ) -> None: @@ -359,14 +439,17 @@ def test_core_vault_scaffold_seeds_sprints_base_fence(tmp_path: Path) -> None: assert view["sort"] == [{"property": "created", "direction": "DESC"}] -def test_core_vault_scaffold_non_empty_destination_aborts(tmp_path: Path) -> None: +def test_core_vault_scaffold_non_empty_destination_fills_the_gaps( + tmp_path: Path, +) -> None: dest = tmp_path / "vault" dest.mkdir() (dest / "stray.md").write_text("x\n") result = _run("scaffold", "core.setup_playbook.scaffold", str(dest), cwd=tmp_path) - assert result.returncode == 1 - assert not (dest / "sprints.md").exists() + assert result.returncode == 0, result.stderr + assert (dest / "sprints.md").exists() + assert (dest / "stray.md").read_text() == "x\n" def test_logs_one_scaffold_line_when_project_attached( From 683a050c14ac86fd54415aa81a21e1d74f6bc4ee Mon Sep 17 00:00:00 2001 From: Anton Shuvalov Date: Sun, 9 Aug 2026 13:52:07 +0700 Subject: [PATCH 02/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 241 ++++++++++++++++++ .../request.md | 49 ++++ 2 files changed, 290 insertions(+) create mode 100644 vault/plans/202608091310_scaffold-seeded-plan-creation/index.md create mode 100644 vault/plans/202608091310_scaffold-seeded-plan-creation/request.md diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md new file mode 100644 index 00000000..fe93cd6f --- /dev/null +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -0,0 +1,241 @@ +--- +title: Scaffold-seeded plan creation and a scaffold receipt contract +type: feature +status: in-progress +sp: 24 +related_to: null +created: 2026-08-09 13:10 +planned: null +started: 2026-08-09 13:46 +completed: null +code_reviews: [] +sessions: +- b1cf1fe7-f86c-4335-9434-c95e92d29a8c +- b74396a9-b99a-442d-9be4-9c58e6c11d5c +retro: null +agents: + cross-review: ad3c866791222e82b +summary: groom seeds index.md via booping scaffold; scaffold and + frontmatter-update both answer with a unified diff +commit: 1bcc6229c7e8e013baec69c28747135663eb23e0 +reviewed_at: 2026-08-09 13:44 +--- + +# Scaffold-seeded plan creation and a scaffold receipt contract + +## Context + +`booping scaffold` materialises a config-declared file tree, rendering each file through the full context environment — so `macro('core.macros.git_commit')` and `core.macros.date` already work inside a tree. `setup` and `playbook-authoring` create their artifacts this way. The plan track does not: `groom/intake` shows the model a literal frontmatter block and asks it to write `index.md` in that shape, transcribing every value by hand. + +After this plan: `groom/intake` creates the plan directory with one `booping scaffold` call, and `booping scaffold` reports each write as a unified diff so the caller knows what landed without reading the file back. `commit:` carries the repo HEAD from the moment the plan is created instead of staying `null` until a sprint starts, and the frontmatter shape lives in exactly one place — the config tree — instead of three. + +## Decisions + +- **Receipt format**: plain unified diff per touched file, `/dev/null` as the from-side for a new file — because the created-versus-updated distinction is already readable from the `---` line, so a separate verb header would restate it. `difflib.unified_diff` from the stdlib, not `git diff --no-index`, because scaffold's primary caller (`setup`) targets a vault that is not a git repository yet. +- **Silence on no-op**: a file whose rendered content matches what is on disk produces no output — the report carries changes, not an inventory. +- **Existing files are skipped, not errors**: without `--force` an existing target is left alone and reported; the caller decides what to do. This replaces the current dest-is-not-empty hard error, which cannot distinguish a stale directory from a plan directory that legitimately already exists. +- **`--force` keeps its meaning**: overwrite the files the tree names, reported as a normal diff. +- **Tree covers `index.md` plus an empty `request.md`**: the brief is free prose with no macro-valued keys, so only its path is seeded. +- **`_partials/plan_frontmatter.md` is deleted, not moved**: `booping frontmatter-update {plan} sp=… summary="…"` already renders its values as Jinja with the `macro` global, so draft-plan writes its two keys through the CLI and needs no literal block to copy. +- **`frontmatter-update` gets the same receipt**: it prints a key list to stderr today, which tells the caller what was set but not what the file now says. Both writers share one diff helper and one silence rule, so a prompt body never needs to know which command produced a receipt. +- **`frontmatter-update` types its scalars**: it stores every value as a string today, so `sp=23` lands as `sp: '23'` while every existing plan carries an integer. Handing draft-plan a writer that quotes numbers would corrupt the key it is being handed, so the coercion is a prerequisite rather than a follow-up. Found by writing this plan's own `sp` through the command. +- **`_partials/plan_structure.md` collapses into `_partials/plan_templates.md`**: once the frontmatter block and the H1 rule are gone — the tree seeds both — what remains is the template mechanics, and draft-plan already includes the two partials back to back. +- **Slug prefixing stays with the model**: the groom preamble already renders `Plan dir: plans/{YYYYMMDDHHmm}_{kebab-title}/`, so no flag and no config dest pattern is added. + +## Architecture + +`booping scaffold ` is invoked from a rendered prompt body and its stdout is read by the model that invoked it. That makes stdout a prompt-facing contract, not a human log: it must be complete enough to skip a read-back and quiet enough not to flood context. + +The write path already separates planning from application — `_plan()` builds a list of `_Write(path, content)` before anything touches disk, so the previous content of every target is readable at that point and a diff needs no restructuring. Only `_apply()` changes: it compares against what is on disk, decides write-versus-skip, and emits the receipt. + +Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffold`. All three see the new report; only groom's prompt is rewritten in this sprint. + +`booping frontmatter-update` is the second half of the same surface: scaffold creates a file, `frontmatter-update` amends one, and after this plan both answer with the same diff. Between them they cover every write groom makes to a plan's frontmatter, which is what lets the shape live only in the config tree. + +## Milestones + +### M1: Scaffold receipt and per-file write semantics — 8 SP | done + +**Goal**: `booping scaffold` prints a unified diff for every file it writes, skips and reports files that already exist, and says nothing about files whose content is unchanged. + +**Verify**: `cd booping-python && uv run pytest tests/commands/scaffold_test.py -q` + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Add the shared receipt helper — `difflib.unified_diff` over previous content (empty for a new file) versus new content, `/dev/null` as the from-file when the target did not exist, the path as the to-file, and the empty string for identical content — and emit it per written file from scaffold | `booping-python/src/booping/utils.py`, `booping-python/src/booping/commands/scaffold.py` | 3 | done | +| 1.2 | Per-file existence semantics — an existing target is skipped and reported without a write, `--force` overwrites and reports the diff, and the dest-is-not-empty precondition is removed in favour of the per-file rule | `booping-python/src/booping/commands/scaffold.py` | 3 | done | +| 1.3 | Cover both in tests — new file diff shape, overwrite diff shape, unchanged file silence, skipped existing file, `--force` overwrite, and a non-empty destination no longer erroring | `booping-python/tests/commands/scaffold_test.py` | 2 | done | + +#### Task 1.1 DoD + +- [x] A newly created file prints `--- /dev/null`, `+++ {path}`, and one all-additions hunk. +- [x] An overwritten file prints a diff of the change alone, with standard context lines. +- [x] A file whose rendered content equals its on-disk content prints nothing. +- [x] Directory creation is still reported, and the trailing count line still summarises the run. +- [x] No new dependency is added to `booping-python/pyproject.toml`. + +#### Task 1.2 DoD + +- [x] An existing target without `--force` is not written and is reported as existing. +- [x] An existing target with `--force` is overwritten and reported as a diff. +- [x] `booping scaffold` against a non-empty destination exits 0 instead of erroring. +- [x] Exit code 2 is still reserved for write failures, and 1 for user errors (bad config path, malformed `--set`). + +#### Task 1.3 DoD + +- [x] Each of the six behaviours above has its own test. +- [x] The three report strings asserted at `tests/commands/scaffold_test.py:208-212` are replaced, not deleted, by assertions on the new shapes. +- [x] `uv run pytest tests/commands/scaffold_test.py -q` passes. + +--- + +### M2: The same receipt from `frontmatter-update` — 5 SP | pending + +**Goal**: `booping frontmatter-update` prints the diff of the change it made, in the same shape scaffold prints, on stdout. + +**Verify**: `bin/booping frontmatter-update {a plan}/index.md summary="probe" && bin/booping frontmatter-update {a plan}/index.md summary="probe"` — the first prints a one-hunk diff, the second prints nothing. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Print the file's before/after diff through the shared helper on stdout, keeping the existing `updated {path}: {keys}` summary on stderr | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | pending | +| 2.2 | Write scalar values with their YAML type — an integer, float, boolean or null value lands unquoted, everything else stays a string | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | pending | +| 2.3 | Cover the diff, the no-op silence, the scalar typing and the `--remove` / `--append` paths in tests | `booping-python/tests/commands/frontmatter_update_test.py` | 1 | pending | + +#### Task 2.1 DoD + +- [ ] A key set to a new value prints a unified diff of the frontmatter lines that changed, and nothing else. +- [ ] Setting a key to the value it already holds prints nothing on stdout. +- [ ] An `--append` of an already-present value prints nothing on stdout. +- [ ] The summary line stays on stderr, so a hook's captured output is unchanged. +- [ ] Exit codes are unchanged: `1` for a missing plan or a malformed pair, `2` for a write failure. + +#### Task 2.2 DoD + +- [ ] The rule is round-trip, not de-quoting: the value is parsed into the Python type its plain YAML form would load as, and the emitter is left to decide quoting. No quote-stripping pass, and no value that reloads as a different type than the one it was written with. +- [ ] `sp=23` writes `sp: 23`, not `sp: '23'`, matching how every existing plan stores it. +- [ ] `retro=null` writes a YAML null, and a boolean value writes unquoted. +- [ ] A value that only looks numeric in part (`summary=23 things`) stays a string. +- [ ] A string whose plain form would reload as another type keeps its quotes — `summary=yes` writes `'yes'`, since bare `yes` reloads as a boolean under the YAML 1.1 resolver ruamel uses. +- [ ] A string needing quotes for syntax keeps them: a leading `@`, `*`, `&`, `!`, `%` or backtick, a colon-space inside the value, or leading or trailing whitespace. +- [ ] A macro-rendered value (`completed="{{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }}"`) still writes as a string, so the date keys hooks stamp are unaffected. + +#### Task 2.3 DoD + +- [ ] Each of the behaviours in 2.1 and 2.2 has its own test. +- [ ] The tests assert the diff's shape, not just that output is non-empty. +- [ ] A regression test pins `sp` round-tripping as an integer. + +--- + +### M3: The groom scaffold tree and intake wiring — 4 SP | pending + +**Goal**: `groom/intake` creates the plan directory with one `booping scaffold` call, and the created `index.md` carries a real `created` and a real `commit`. + +**Verify**: `bin/booping scaffold core.groom_playbook.scaffold /tmp/plan-tree-check --set title="Check" --set type=feature` then read the printed diff — no file is read back. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 3.1 | Add `core.groom_playbook.scaffold` — `index.md` carrying the identity frontmatter with `title`/`type` from `--set`, `created` from `core.macros.date`, `commit` from `core.macros.git_commit`, every other key at its documented empty value, plus the H1; and an empty `request.md` | `src/config.yaml` | 2 | pending | +| 3.2 | Rewrite intake's plan-creation section to invoke the tree and act on the printed receipt, dropping the frontmatter block and its include | `playbooks/groom/intake/fable-5.md` | 2 | pending | + +#### Task 3.1 DoD + +- [ ] The tree renders with `--set title=` and `--set type=` and no other variables. +- [ ] `created` and `commit` come from macros; neither is a placeholder. +- [ ] `status:` is present as `framing`, keeping `playbook-transition`'s initial-status bootstrap satisfied. +- [ ] `retro:` is `null`, since the retro candidates query filters on it. +- [ ] `sp`, `related_to`, `planned`, `started`, `completed` are `null`; `code_reviews` and `sessions` are `[]`. +- [ ] A title containing a colon or a quote renders as valid YAML — the tree is raw text templating with no emitter to lean on, so the value is quoted at the template level (`{{ title | tojson }}`). + +#### Task 3.2 DoD + +- [ ] The step body names the exact invocation, with the plan directory assembled from the preamble's rendered `Plan dir:` line. +- [ ] The body states that the printed receipt is the confirmation and the created file is not read back. +- [ ] A target reported as already existing is handled by the step's own judgement, with no branch prescribed in the prompt. +- [ ] The `{% include "_partials/plan_frontmatter.md" %}` line is gone. + +--- + +### M4: Retire the frontmatter duplicates — 5 SP | pending + +**Goal**: the plan frontmatter shape exists only in `core.groom_playbook.scaffold`, and draft-plan writes its keys through `booping frontmatter-update`. + +**Verify**: `grep -rn "plan_frontmatter\|template_plan_frontmatter\|plan_structure" playbooks/ docs/ src/ | grep -v "_reports/\|_specs/"` returns nothing. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 4.1 | Delete `_partials/plan_frontmatter.md` and `_partials/plan_structure.md`, folding the surviving template mechanics — the two top-level template sections, and authoring a new template when none fits — into `_partials/plan_templates.md`, which also names the `frontmatter-update` invocation that writes the drafter's keys | `playbooks/_partials/plan_frontmatter.md`, `playbooks/_partials/plan_structure.md`, `playbooks/_partials/plan_templates.md`, `playbooks/groom/draft-plan/opus-5.md` | 3 | pending | +| 4.2 | Delete `docs/template_plan_frontmatter.md` and repoint the Quality Checklist line in all five plan templates at the observable property instead of the deleted file | `docs/template_plan_frontmatter.md`, `docs/plan_templates/backend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md`, `docs/plan_templates/frontend.md` | 2 | pending | + +#### Task 4.1 DoD + +- [ ] `playbooks/_partials/plan_frontmatter.md` and `playbooks/_partials/plan_structure.md` are deleted, and `draft-plan/opus-5.md` includes only `_partials/plan_templates.md`. +- [ ] `plan_templates.md` carries the `# Plan Body` / `# Quality Checklist` mechanics and the author-a-new-template rule, unchanged in substance. +- [ ] `plan_templates.md` states that `sp` and `summary` are the drafter's and are written with `booping frontmatter-update {plan}/index.md sp=… summary="…"`. +- [ ] No partial contains a frontmatter yaml block or an H1-matching-title rule. +- [ ] `bin/booping render-playbook groom` exits 0 and its Draft Plan section renders without a template error. + +#### Task 4.2 DoD + +- [ ] `docs/template_plan_frontmatter.md` is deleted. +- [ ] No file references `template_plan_frontmatter`. +- [ ] Each plan template's frontmatter checklist item asserts a property that can be checked against the plan itself. + +--- + +### M5: Documentation — 2 SP | pending + +**Goal**: the scaffold documentation describes three trees and the stdout contract both writers now share. + +**Verify**: `just docs` + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 5.1 | Update the scaffold section — the third tree and its `--set` variables, the unified-diff stdout contract shared with `frontmatter-update`, the per-file skip rule and what `--force` now means — and correct the "two scaffold trees" count | `documentation/project_config.md` | 2 | pending | + +#### Task 5.1 DoD + +- [ ] `core.groom_playbook.scaffold` is listed beside the other two trees with its `--set` variables. +- [ ] The stdout contract is documented as a unified diff per changed file, silent on unchanged files. +- [ ] The skip-existing rule and `--force`'s meaning are stated. +- [ ] The sentence counting "the two scaffold trees" reads three. +- [ ] `just docs` builds without a broken-link warning on the edited page. + +--- + +## I/O contract + +- **Arguments / flags**: unchanged — `booping scaffold [--force] [--set KEY=VALUE]... [--stub-macro DOTTED.PATH=LITERAL]...`. +- **stdin**: not read. +- **stdout**: for each file the run wrote, a unified diff — `--- /dev/null` or `--- {path}`, `+++ {path}`, then hunks. Created directories keep their existing one-line report. The run ends with the existing count line. A file whose content did not change, and a file skipped because it exists, contribute no diff; a skipped file is named on one line. +- **stderr**: unchanged — `error: {message}` for user errors and write failures. +- **Exit codes**: `0` success, including a destination that already holds some of the tree's files; `1` user error (unknown config path, malformed `--set` or `--stub-macro`, destination exists as a non-directory); `2` write failure. + +`booping frontmatter-update` adopts the same stdout contract — a unified diff of the change it made, nothing when the file did not change — while keeping its `updated {path}: {keys}` summary and its error messages on stderr. Its arguments, flags and exit codes are unchanged. + +## Final Verification + +- [ ] `just ci` passes. +- [ ] `just snapshots` is run and its diff reported verbatim; `just snapshots-accept` is never run, since accepting the baseline is the user's call. +- [ ] `bin/booping scaffold --help` reflects the unchanged flag surface. +- [ ] A scaffold into a fresh directory and a second scaffold into the same directory are both exercised, and the second writes nothing. +- [ ] `bin/booping render-playbook groom` exits 0 with no STOP notice. +- [ ] A real groom run creates a plan whose `commit:` equals `git rev-parse HEAD`. +- [ ] `bin/booping frontmatter-update` on an unchanged value prints nothing on stdout and still exits 0. + +## Out of scope + +- `develop/intake`'s missing `commit: null` branch and its equality comparison of plan `commit` against HEAD. Plans created before this change keep the `null..HEAD` behaviour. +- The `goal:` key reconciliation between retro's `close-working-set` stamp and `gather-feedback`'s read. +- Converting `setup` and `playbook-authoring` prompt bodies to consume the new receipt. Their stdout changes with the CLI, but their bodies are not rewritten here. +- Any change to slug construction — no flag, no config dest pattern. + +## Risk register + +- **Dropping the dest-is-not-empty precondition changes `setup` and `playbook-authoring`.** Both currently refuse a half-built destination and will instead fill the gaps, with `--force` still the only way to overwrite. Accepted deliberately: the per-file rule is strictly more informative, and refusing was what blocked a plan directory that legitimately already exists. +- **The receipt is prompt-facing, so its size is a context cost.** Mitigated by the silence rules rather than a cap: only changed files appear, and a diff carries only its hunks. + +## CLAUDE.md impact + +- `## Config` — the scaffold-tree bullet gains the groom tree; the existing wording generalises, so confirm rather than assume no edit is needed. +- No change to `## Layout`, `## Playbooks` or `## Lifecycle`: no file moves between roots and no status vocabulary changes. diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/request.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/request.md new file mode 100644 index 00000000..e6903631 --- /dev/null +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/request.md @@ -0,0 +1,49 @@ +## Request + +> groom this. And also {vault}/plans/{slug} - slug can be partially applied with `YYYYMMDDHHmm-` prefix. And need to decide what model must get back from scaffold call, to avoid double-checking created file. Mb this per file? +> +> ``` +> --- +> [CREATED|UPDATED]: {path} +> +> {diff} +> --- +> ``` + +"groom this" carries the preceding conversation: seed the plan's `index.md` through `booping scaffold` instead of having the model transcribe a frontmatter block from the prompt. + +## Task type + +`feature` — new capability on existing surfaces: groom gains a scaffold-seeded plan creation path, `scaffold` gains a per-file receipt contract callers consume instead of re-reading files. + +- Not `bug` — the `commit: null` gap at develop's intake is real, but it comes from a key with no producer, not a producer misbehaving. Fixed by adding the seed, not repairing an existing path. +- Not `refactoring` — observable surface changes: `scaffold` prints a different report, plan frontmatter carries values it never carried at creation, the groom prompt loses a block. No no-behaviour-change DoD is writable. + +## Problem + +`groom/intake` shows the model a literal frontmatter block (`_partials/plan_frontmatter.md`) and tells it to create `index.md` in that shape. Every value is transcribed by the model, `created` included. + +1. **`commit:` has no producer at creation.** Stamped only by develop's `ready-for-dev → in-progress` hook, which fires at `provision` — after `develop/intake`, which reads it as its drift baseline and runs `git diff --name-only {plan.commit}..HEAD`. First sprint interpolates `null`. The check works only on re-entry, then against the previous sprint's start rather than what the plan was designed against. +2. **The shape lives in prose.** Included twice — intake, and via `plan_structure.md` at draft-plan — as a literal in the prompt. Nothing validates that what the model wrote matches it. +3. **Transcription risk on compared values.** A 40-char SHA copied by a model and compared by equality fails silently: a truncated copy is never equal, `{plan.commit}..HEAD` still resolves, so drift is reported forever without an error. + +The primitive exists. `booping scaffold` renders each file through `build_source_env`, which registers `macro` as a Jinja global — `macro('core.macros.git_commit')` and `core.macros.date` work inside a scaffold tree today. `setup` and `playbook-authoring` already create their artifacts this way; the plan track does not. + +One sub-problem the request names stays in scope: + +- **Return contract.** `scaffold` prints `created file {path}` / `overwrote file {path}` plus a count line. That says a file exists, not what is in it, so a caller reads it back — the exact context cost this change removes. + +The other — applying the `YYYYMMDDHHmm` prefix to the plan directory for the model — was considered and dropped: the preamble already renders `Plan dir: plans/{YYYYMMDDHHmm}_{kebab-title}/`, so the model substitutes the kebab title and passes the whole path. No CLI flag, no config dest pattern. + +## Clarifications and Decisions + +- Seeding at creation is not the hand-editing the driving protocol forbids: `status: framing` is already seeded that way, and load-bearing — `playbook-transition` rejects any target but the initial status when the artifact carries none. +- `commit:` seeded at groom is framing-time HEAD, not designed-against HEAD. Fails safe: an earlier base over-reports drift, never under-reports, and develop's intake filters by plan-touched files first. +- develop's `ready-for-dev → in-progress` hook keeps re-stamping `commit:` to sprint start — code-review's diff base unchanged. +- `retro:` stays `null` in the seed — it is the retro queue predicate (`where: {retro: null}`). +- The frontmatter partial was already edited this session (`sp: null`, `related_to: null`, `code_reviews: []`, `goal` and `split_from` dropped). That edit is the starting point, not part of the work. +- The receipt is diff-shaped, not verb-plus-body: a created file is a diff against nothing, and the CREATED/UPDATED distinction is readable from the diff itself rather than carried as a separate header line. Exact shape settles at design alignment. +- Slug prefixing stays with the model — the render already supplies the prefix. +- Sprint scope is the core only: the groom seed tree, intake calling it, the frontmatter partial leaving the prompt, and scaffold's report. Explicitly out: develop/intake's missing `commit: null` branch and its equality comparison, the `goal:` key reconciliation, and converting setup / playbook-authoring to the new receipt. +- Consequence accepted: with develop/intake untouched, the seed makes `commit:` present on new plans only — plans created before this change keep the `null..HEAD` behaviour. +- No post-implementation reshape milestone. From 8b200d188ef5d1d3269148739f5693b05cce0b13 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov Date: Sun, 9 Aug 2026 13:59:27 +0700 Subject: [PATCH 03/44] =?UTF-8?q?feat(booping):=20scaffold-seeded=20plan?= =?UTF-8?q?=20creation=20=E2=80=94=20frontmatter-update=20prints=20the=20s?= =?UTF-8?q?ame=20diff=20receipt=20and=20types=20its=20scalars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booping/commands/frontmatter_update.py | 57 +++++++- .../tests/commands/frontmatter_update_test.py | 126 ++++++++++++++++++ 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/booping-python/src/booping/commands/frontmatter_update.py b/booping-python/src/booping/commands/frontmatter_update.py index 2933dd9c..db3dacc9 100644 --- a/booping-python/src/booping/commands/frontmatter_update.py +++ b/booping-python/src/booping/commands/frontmatter_update.py @@ -6,12 +6,19 @@ from pathlib import Path from typing import Any +import yaml from jinja2 import Environment, TemplateError +from ruamel.yaml import YAML as RuamelYAML +from ruamel.yaml.error import YAMLError as RuamelYAMLError +from ruamel.yaml.scalarstring import SingleQuotedScalarString from booping import logger from booping.context import Context from booping.context._yaml import update_frontmatter from booping.macros import MacroError, make_macro +from booping.utils import diff_report + +_NULL_FORMS = {"null", "Null", "NULL", "~"} def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: # type: ignore[type-arg] @@ -80,6 +87,41 @@ def interpolate( sys.exit(2) +def coerce_scalar(value: str) -> object: + """Return *value* as the Python type its plain YAML form loads as. + + An integer, float, boolean or null form becomes that type, so the emitter + writes it unquoted. Everything else stays a string; a string whose plain + form would reload as another type under the YAML 1.1 resolver the readers + use is marked single-quoted, so it reloads as the string it was written as. + """ + if not value.strip(): + return value + + try: + loaded: object = RuamelYAML(typ="rt").load(value) # type: ignore[reportUnknownMemberType] + except RuamelYAMLError: + loaded = value + + if loaded is None: + # An anchor or an empty tag also loads as None; only the null spellings + # are a real null. + return None if value.strip() in _NULL_FORMS else _quote_if_ambiguous(value) + if isinstance(loaded, bool | int | float): + return loaded + return _quote_if_ambiguous(value) + + +def _quote_if_ambiguous(value: str) -> str: + try: + reloaded: object = yaml.safe_load(value) + except yaml.YAMLError: + return value + if isinstance(reloaded, str) and reloaded == value: + return value + return SingleQuotedScalarString(value) + + def parse_pairs(pairs: list[str]) -> dict[str, str]: updates: dict[str, str] = {} for pair in pairs: @@ -117,13 +159,20 @@ def _run(args: argparse.Namespace) -> None: vault_dir = project.directory if project is not None else None resolved: dict[str, object] = {} + # The summary echoes the interpolated text, not the coerced value, so the + # stderr line reads the same as before the typing rule. + summary_values: dict[str, str] = {} for key, value in updates.items(): - resolved[key] = interpolate(value, repo_dir, ctx.config, vault_dir) + rendered = interpolate(value, repo_dir, ctx.config, vault_dir) + summary_values[key] = rendered + resolved[key] = coerce_scalar(rendered) resolved_appends: dict[str, object] = {} for key, value in appends.items(): resolved_appends[key] = interpolate(value, repo_dir, ctx.config, vault_dir) + previous = plan_path.read_text() + try: update_frontmatter(plan_path, resolved, removals=removals, appends=resolved_appends) except ValueError as exc: @@ -133,6 +182,10 @@ def _run(args: argparse.Namespace) -> None: print(f"error: {exc}", file=sys.stderr) sys.exit(2) + report = diff_report(plan_path, previous, plan_path.read_text()) + if report: + print(report) + vault = project.directory if project is not None else None changed = [f"-{k}" for k in removals] + list(resolved.keys()) + list(resolved_appends.keys()) logger.log( @@ -141,7 +194,7 @@ def _run(args: argparse.Namespace) -> None: parts = ( [f"-{k}" for k in removals] - + [f"{k}={v}" for k, v in resolved.items()] + + [f"{k}={v}" for k, v in summary_values.items()] + [f"{k}+={v}" for k, v in resolved_appends.items()] ) print(f"updated {plan_path}: {', '.join(parts)}", file=sys.stderr) \ No newline at end of file diff --git a/booping-python/tests/commands/frontmatter_update_test.py b/booping-python/tests/commands/frontmatter_update_test.py index 85b4f7c4..ceb17008 100644 --- a/booping-python/tests/commands/frontmatter_update_test.py +++ b/booping-python/tests/commands/frontmatter_update_test.py @@ -188,6 +188,66 @@ def test_file_target_with_quoted_macro_expression(self, tmp_path: Path) -> None: # ── CLI integration ────────────────────────────────────────────────────── +class TestScalarTyping: + def _write(self, tmp_path: Path, pair: str) -> str: + plan = _make_plan(tmp_path, "title: Foo") + fu_cmd._run(_ns(plan=plan, pairs=[pair])) # type: ignore[reportPrivateUsage] + _, fm_text, _ = _split_result(plan.read_text()) + return fm_text + + def test_integer_lands_unquoted(self, tmp_path: Path) -> None: + assert "sp: 23\n" in self._write(tmp_path, "sp=23") + + def test_integer_round_trips_as_an_integer(self, tmp_path: Path) -> None: + import yaml as pyyaml + + assert pyyaml.safe_load(self._write(tmp_path, "sp=23"))["sp"] == 23 + + def test_float_lands_unquoted(self, tmp_path: Path) -> None: + assert "ratio: 1.5\n" in self._write(tmp_path, "ratio=1.5") + + def test_null_lands_as_yaml_null(self, tmp_path: Path) -> None: + assert "retro: null\n" in self._write(tmp_path, "retro=null") + + def test_boolean_lands_unquoted(self, tmp_path: Path) -> None: + assert "blocked: true\n" in self._write(tmp_path, "blocked=true") + + def test_partly_numeric_value_stays_a_string(self, tmp_path: Path) -> None: + import yaml as pyyaml + + fm_text = self._write(tmp_path, "summary=23 things") + assert "summary: 23 things\n" in fm_text + assert pyyaml.safe_load(fm_text)["summary"] == "23 things" + + def test_yaml_1_1_boolean_word_keeps_its_quotes(self, tmp_path: Path) -> None: + import yaml as pyyaml + + fm_text = self._write(tmp_path, "summary=yes") + assert "summary: 'yes'\n" in fm_text + assert pyyaml.safe_load(fm_text)["summary"] == "yes" + + @pytest.mark.parametrize( + "value", + ["@lead", "*star", "&anchor", "!bang", "%pct", "`tick", "a: b", " padded "], + ) + def test_syntax_sensitive_string_keeps_its_quotes(self, tmp_path: Path, value: str) -> None: + import yaml as pyyaml + + fm_text = self._write(tmp_path, f"summary={value}") + assert pyyaml.safe_load(fm_text)["summary"] == value + + def test_macro_rendered_date_stays_a_string(self, tmp_path: Path) -> None: + import yaml as pyyaml + + plan = _make_plan(tmp_path, "title: Foo") + fu_cmd._run(_ns(plan=plan, pairs=[f"completed={NOW_EXPR}"])) # type: ignore[reportPrivateUsage] + + _, fm_text, _ = _split_result(plan.read_text()) + completed = pyyaml.safe_load(fm_text)["completed"] + assert isinstance(completed, str) + datetime.strptime(completed, "%Y-%m-%d %H:%M") # noqa: DTZ007 + + class TestFrontmatterUpdateCLI: def test_sets_planned_with_date_macro(self, tmp_path: Path) -> None: plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") @@ -415,6 +475,72 @@ def test_append_only_still_reports_nothing_to_do_when_empty( assert excinfo.value.code == 1 assert "nothing to do" in capsys.readouterr().err + def test_prints_a_unified_diff_of_the_change( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") + + fu_cmd._run(_ns(plan=plan, pairs=["status=in-progress"])) # type: ignore[reportPrivateUsage] + + lines = capsys.readouterr().out.splitlines() + assert lines[0] == f"--- {plan}" + assert lines[1] == f"+++ {plan}" + assert lines[2].startswith("@@") + assert "-status: backlog" in lines + assert "+status: in-progress" in lines + assert "+title: Foo" not in lines + + def test_unchanged_value_prints_nothing( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") + + fu_cmd._run(_ns(plan=plan, pairs=["status=backlog"])) # type: ignore[reportPrivateUsage] + + assert capsys.readouterr().out == "" + + def test_idempotent_append_prints_nothing( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nsessions:\n- abc-123") + + fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] + + assert capsys.readouterr().out == "" + + def test_append_prints_the_added_line( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nsessions:\n- abc-123") + + fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=def-456"])) # type: ignore[reportPrivateUsage] + + lines = capsys.readouterr().out.splitlines() + assert lines[0] == f"--- {plan}" + assert "+- def-456" in lines + + def test_removal_prints_the_dropped_line( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nbusiness_goal: x\nstatus: backlog") + + fu_cmd._run(_ns(plan=plan, pairs=[], removals=["business_goal"])) # type: ignore[reportPrivateUsage] + + lines = capsys.readouterr().out.splitlines() + assert lines[0] == f"--- {plan}" + assert "-business_goal: x" in lines + + def test_summary_stays_on_stderr( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") + + fu_cmd._run(_ns(plan=plan, pairs=["status=in-progress"])) # type: ignore[reportPrivateUsage] + + captured = capsys.readouterr() + assert captured.err == f"updated {plan}: status=in-progress\n" + assert "updated" not in captured.out + def test_logs_to_booping_log(self, tmp_path: Path) -> None: """Log line appended to .booping.log.""" vault = tmp_path / "vault" From c84f64e6775b8e603ccf21869b03db3dc0b17a27 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov Date: Sun, 9 Aug 2026 13:59:27 +0700 Subject: [PATCH 04/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index fe93cd6f..c294252b 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -89,7 +89,7 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo --- -### M2: The same receipt from `frontmatter-update` — 5 SP | pending +### M2: The same receipt from `frontmatter-update` — 5 SP | done **Goal**: `booping frontmatter-update` prints the diff of the change it made, in the same shape scaffold prints, on stdout. @@ -97,33 +97,33 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 2.1 | Print the file's before/after diff through the shared helper on stdout, keeping the existing `updated {path}: {keys}` summary on stderr | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | pending | -| 2.2 | Write scalar values with their YAML type — an integer, float, boolean or null value lands unquoted, everything else stays a string | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | pending | -| 2.3 | Cover the diff, the no-op silence, the scalar typing and the `--remove` / `--append` paths in tests | `booping-python/tests/commands/frontmatter_update_test.py` | 1 | pending | +| 2.1 | Print the file's before/after diff through the shared helper on stdout, keeping the existing `updated {path}: {keys}` summary on stderr | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | done | +| 2.2 | Write scalar values with their YAML type — an integer, float, boolean or null value lands unquoted, everything else stays a string | `booping-python/src/booping/commands/frontmatter_update.py` | 2 | done | +| 2.3 | Cover the diff, the no-op silence, the scalar typing and the `--remove` / `--append` paths in tests | `booping-python/tests/commands/frontmatter_update_test.py` | 1 | done | #### Task 2.1 DoD -- [ ] A key set to a new value prints a unified diff of the frontmatter lines that changed, and nothing else. -- [ ] Setting a key to the value it already holds prints nothing on stdout. -- [ ] An `--append` of an already-present value prints nothing on stdout. -- [ ] The summary line stays on stderr, so a hook's captured output is unchanged. -- [ ] Exit codes are unchanged: `1` for a missing plan or a malformed pair, `2` for a write failure. +- [x] A key set to a new value prints a unified diff of the frontmatter lines that changed, and nothing else. +- [x] Setting a key to the value it already holds prints nothing on stdout. +- [x] An `--append` of an already-present value prints nothing on stdout. +- [x] The summary line stays on stderr, so a hook's captured output is unchanged. +- [x] Exit codes are unchanged: `1` for a missing plan or a malformed pair, `2` for a write failure. #### Task 2.2 DoD -- [ ] The rule is round-trip, not de-quoting: the value is parsed into the Python type its plain YAML form would load as, and the emitter is left to decide quoting. No quote-stripping pass, and no value that reloads as a different type than the one it was written with. -- [ ] `sp=23` writes `sp: 23`, not `sp: '23'`, matching how every existing plan stores it. -- [ ] `retro=null` writes a YAML null, and a boolean value writes unquoted. -- [ ] A value that only looks numeric in part (`summary=23 things`) stays a string. -- [ ] A string whose plain form would reload as another type keeps its quotes — `summary=yes` writes `'yes'`, since bare `yes` reloads as a boolean under the YAML 1.1 resolver ruamel uses. -- [ ] A string needing quotes for syntax keeps them: a leading `@`, `*`, `&`, `!`, `%` or backtick, a colon-space inside the value, or leading or trailing whitespace. -- [ ] A macro-rendered value (`completed="{{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }}"`) still writes as a string, so the date keys hooks stamp are unaffected. +- [x] The rule is round-trip, not de-quoting: the value is parsed into the Python type its plain YAML form would load as, and the emitter is left to decide quoting. No quote-stripping pass, and no value that reloads as a different type than the one it was written with. +- [x] `sp=23` writes `sp: 23`, not `sp: '23'`, matching how every existing plan stores it. +- [x] `retro=null` writes a YAML null, and a boolean value writes unquoted. +- [x] A value that only looks numeric in part (`summary=23 things`) stays a string. +- [x] A string whose plain form would reload as another type keeps its quotes — `summary=yes` writes `'yes'`, since bare `yes` reloads as a boolean under the YAML 1.1 resolver ruamel uses. +- [x] A string needing quotes for syntax keeps them: a leading `@`, `*`, `&`, `!`, `%` or backtick, a colon-space inside the value, or leading or trailing whitespace. +- [x] A macro-rendered value (`completed="{{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }}"`) still writes as a string, so the date keys hooks stamp are unaffected. #### Task 2.3 DoD -- [ ] Each of the behaviours in 2.1 and 2.2 has its own test. -- [ ] The tests assert the diff's shape, not just that output is non-empty. -- [ ] A regression test pins `sp` round-tripping as an integer. +- [x] Each of the behaviours in 2.1 and 2.2 has its own test. +- [x] The tests assert the diff's shape, not just that output is non-empty. +- [x] A regression test pins `sp` round-tripping as an integer. --- From aca681f39c42b3b2785b712a28ff508c1dc8a5ca Mon Sep 17 00:00:00 2001 From: Anton Shuvalov Date: Sun, 9 Aug 2026 14:02:44 +0700 Subject: [PATCH 05/44] =?UTF-8?q?feat(groom):=20scaffold-seeded=20plan=20c?= =?UTF-8?q?reation=20=E2=80=94=20seed=20the=20plan=20directory=20from=20co?= =?UTF-8?q?re.groom=5Fplaybook.scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- playbooks/groom/intake/fable-5.md | 19 +++++++++++-------- src/config.yaml | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/playbooks/groom/intake/fable-5.md b/playbooks/groom/intake/fable-5.md index c6681428..8915222c 100644 --- a/playbooks/groom/intake/fable-5.md +++ b/playbooks/groom/intake/fable-5.md @@ -9,19 +9,22 @@ Check for context, in case a request is related to already existing plan. {% set query_plans = 'core.groom_playbook.queries.latest_plans' -%} {% include "_partials/plans_table.md" %} -## Task Types +## Task Type -Exactly one per plan. Pick the row the request meets, load its guidance before framing, and rule -the siblings out by name. +Exactly one per plan. Pick the row the request meets, load its guidance before framing and pick one. {% include "_partials/task_types.md" %} -## The plan's identity frontmatter +## The plan directory -Create `index.md` carrying the shape below. `created` is the run's clock to the minute, not just -the date — copy the value verbatim. `status` is the run machine's and is written by -`booping playbook-transition`, never by hand. +Create it in one call, with `{plan-dir}` the preamble's `Plan dir:` line resolved against the +vault and `{title}` the plan's descriptive title: -{% include "_partials/plan_frontmatter.md" %} +``` +booping scaffold core.groom_playbook.scaffold {plan-dir} --set title="{title}" --set type={type} +``` + +The printed diff is the confirmation — do not read the created files back. A target reported as +already existing was not written; decide what that means for this run. ## The brief — written to `request.md`, posted in chat diff --git a/src/config.yaml b/src/config.yaml index 45387076..64cfcc95 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -90,6 +90,32 @@ core: # Agent that performs the detached second-model review of a drafted plan. # Null → the reviewing step is skipped entirely. cross_review_agent: null + # Scaffold tree materialised by + # `booping scaffold core.groom_playbook.scaffold {vault}/plans/ --set title= --set type=<type>`. + # Sole definition of a plan's identity frontmatter. `title` goes through + # `tojson` because seed content is raw text with no YAML emitter behind it, so + # a colon or a quote in the title would otherwise break the document. + scaffold: + index.md: | + --- + title: {{ title | tojson }} + type: {{ type }} + status: framing + sp: null + related_to: null + created: {{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }} + planned: null + started: null + completed: null + code_reviews: [] + sessions: [] + retro: null + summary: null + commit: {{ macro('core.macros.git_commit') }} + --- + + # {{ title }} + request.md: "" agents: booping-researcher: internal: true From 579639ef674c5fe0b362195e16dc7c068ad5fb52 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:02:44 +0700 Subject: [PATCH 06/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index c294252b..767b83d2 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -127,7 +127,7 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo --- -### M3: The groom scaffold tree and intake wiring — 4 SP | pending +### M3: The groom scaffold tree and intake wiring — 4 SP | done **Goal**: `groom/intake` creates the plan directory with one `booping scaffold` call, and the created `index.md` carries a real `created` and a real `commit`. @@ -135,24 +135,24 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 3.1 | Add `core.groom_playbook.scaffold` — `index.md` carrying the identity frontmatter with `title`/`type` from `--set`, `created` from `core.macros.date`, `commit` from `core.macros.git_commit`, every other key at its documented empty value, plus the H1; and an empty `request.md` | `src/config.yaml` | 2 | pending | -| 3.2 | Rewrite intake's plan-creation section to invoke the tree and act on the printed receipt, dropping the frontmatter block and its include | `playbooks/groom/intake/fable-5.md` | 2 | pending | +| 3.1 | Add `core.groom_playbook.scaffold` — `index.md` carrying the identity frontmatter with `title`/`type` from `--set`, `created` from `core.macros.date`, `commit` from `core.macros.git_commit`, every other key at its documented empty value, plus the H1; and an empty `request.md` | `src/config.yaml` | 2 | done | +| 3.2 | Rewrite intake's plan-creation section to invoke the tree and act on the printed receipt, dropping the frontmatter block and its include | `playbooks/groom/intake/fable-5.md` | 2 | done | #### Task 3.1 DoD -- [ ] The tree renders with `--set title=` and `--set type=` and no other variables. -- [ ] `created` and `commit` come from macros; neither is a placeholder. -- [ ] `status:` is present as `framing`, keeping `playbook-transition`'s initial-status bootstrap satisfied. -- [ ] `retro:` is `null`, since the retro candidates query filters on it. -- [ ] `sp`, `related_to`, `planned`, `started`, `completed` are `null`; `code_reviews` and `sessions` are `[]`. -- [ ] A title containing a colon or a quote renders as valid YAML — the tree is raw text templating with no emitter to lean on, so the value is quoted at the template level (`{{ title | tojson }}`). +- [x] The tree renders with `--set title=` and `--set type=` and no other variables. +- [x] `created` and `commit` come from macros; neither is a placeholder. +- [x] `status:` is present as `framing`, keeping `playbook-transition`'s initial-status bootstrap satisfied. +- [x] `retro:` is `null`, since the retro candidates query filters on it. +- [x] `sp`, `related_to`, `planned`, `started`, `completed` are `null`; `code_reviews` and `sessions` are `[]`. +- [x] A title containing a colon or a quote renders as valid YAML — the tree is raw text templating with no emitter to lean on, so the value is quoted at the template level (`{{ title | tojson }}`). #### Task 3.2 DoD -- [ ] The step body names the exact invocation, with the plan directory assembled from the preamble's rendered `Plan dir:` line. -- [ ] The body states that the printed receipt is the confirmation and the created file is not read back. -- [ ] A target reported as already existing is handled by the step's own judgement, with no branch prescribed in the prompt. -- [ ] The `{% include "_partials/plan_frontmatter.md" %}` line is gone. +- [x] The step body names the exact invocation, with the plan directory assembled from the preamble's rendered `Plan dir:` line. +- [x] The body states that the printed receipt is the confirmation and the created file is not read back. +- [x] A target reported as already existing is handled by the step's own judgement, with no branch prescribed in the prompt. +- [x] The `{% include "_partials/plan_frontmatter.md" %}` line is gone. --- From b949ce54a961557ef87ff86b417aa98c765883be Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:06:26 +0700 Subject: [PATCH 07/44] =?UTF-8?q?refactor(groom):=20scaffold-seeded=20plan?= =?UTF-8?q?=20creation=20=E2=80=94=20retire=20the=20duplicated=20plan=20fr?= =?UTF-8?q?ontmatter=20partials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plan_templates/backend.md | 6 ++--- docs/plan_templates/claude_skill.md | 6 ++--- docs/plan_templates/cli.md | 6 ++--- docs/plan_templates/documentation.md | 6 ++--- docs/plan_templates/frontend.md | 6 ++--- docs/template_plan_frontmatter.md | 18 -------------- playbooks/_partials/plan_frontmatter.md | 18 -------------- playbooks/_partials/plan_structure.md | 33 ------------------------- playbooks/_partials/plan_templates.md | 20 +++++++++++++++ playbooks/groom/draft-plan/opus-5.md | 5 +--- 10 files changed, 36 insertions(+), 88 deletions(-) delete mode 100644 docs/template_plan_frontmatter.md delete mode 100644 playbooks/_partials/plan_frontmatter.md delete mode 100644 playbooks/_partials/plan_structure.md diff --git a/docs/plan_templates/backend.md b/docs/plan_templates/backend.md index 8f0a6364..99ac0f4a 100644 --- a/docs/plan_templates/backend.md +++ b/docs/plan_templates/backend.md @@ -28,7 +28,7 @@ How this change fits the existing system. Integration points. Reference concrete **Goal**: one sentence — what changes after this milestone. -**Verify**: exact commands (or observable outcomes) to confirm this milestone is done. +**Verify**: exact commands (or observable outcomes) to confirm this milestone is done, scoped to what this milestone changed — a targeted test path, one invocation, a diff. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| @@ -123,7 +123,7 @@ Verify before leaving `in-spec`. Every item must be satisfiable by reading the p ## Frontmatter -- [ ] Frontmatter matches [plan frontmatter](${CLAUDE_PLUGIN_ROOT}/docs/template_plan_frontmatter.md) — every required field present and shaped correctly. +- [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. - [ ] `sp` equals the sum of per-task SP across milestones. - [ ] `summary` is set (non-empty, single line, ≤ ~120 chars) for `feature` and `refactoring` plans. @@ -133,7 +133,7 @@ Verify before leaving `in-spec`. Every item must be satisfiable by reading the p - [ ] For features and refactorings: `summary` is phrased as the user/internal-visible outcome, not engineering output. - [ ] Definition of Done bullets are testable (verifiable by command or inspectable output). - [ ] Decisions table lists real alternatives — no empty "Alternative considered" rows. -- [ ] Every milestone has a `Verify` command or verifiable outcome. +- [ ] Every milestone has a `Verify` command or verifiable outcome, scoped to what that milestone changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. - [ ] Every task lists exact file paths, not "related files" or "somewhere in X". - [ ] Every task DoD uses checkboxes, not prose. - [ ] Code sketches use `...` in method bodies — agents implement from interfaces, not by copying literal code. diff --git a/docs/plan_templates/claude_skill.md b/docs/plan_templates/claude_skill.md index f9772a5c..19707b1e 100644 --- a/docs/plan_templates/claude_skill.md +++ b/docs/plan_templates/claude_skill.md @@ -25,7 +25,7 @@ How the skill interacts with other skills via shared config (statuses, agents, t **Goal**: one sentence — the observable change in the rendered skill or shared config. -**Verify**: render and sanity check (e.g. `bin/booping render src/templates/skills/<name>.md.j2` and review output). +**Verify**: render and sanity check (e.g. `bin/booping render src/templates/skills/<name>.md.j2` and review output) — the rebuild plus the surfaces this milestone touched. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| @@ -59,7 +59,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat ## Frontmatter -- [ ] Frontmatter matches [plan frontmatter](${CLAUDE_PLUGIN_ROOT}/docs/template_plan_frontmatter.md). +- [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. - [ ] `sp` equals the sum of per-task SP across milestones. ## Content @@ -68,7 +68,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat - [ ] DoD bullets are verifiable by reading the rendered output or a diff. - [ ] Every task lists exact template / partial / config paths. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` step that includes a rebuild. +- [ ] Every milestone has a `Verify` step that includes a rebuild and stays scoped to the surfaces it touched — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. - [ ] Each milestone executable from a fresh session with only the plan as context. ## Skill-design hygiene diff --git a/docs/plan_templates/cli.md b/docs/plan_templates/cli.md index d86dccbe..c619b404 100644 --- a/docs/plan_templates/cli.md +++ b/docs/plan_templates/cli.md @@ -25,7 +25,7 @@ How the CLI fits the surrounding toolchain. Input sources, output sinks, side ef **Goal**: one sentence — the observable change in CLI behavior. -**Verify**: exact invocation + expected output (e.g. `./bin/mytool --flag arg 2>&1 | diff - tests/fixtures/expected.txt`). +**Verify**: exact invocation + expected output (e.g. `./bin/mytool --flag arg 2>&1 | diff - tests/fixtures/expected.txt`). Scoped to this milestone — whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| @@ -70,7 +70,7 @@ Name sections to update (new CLI in the `CLI` section, new inlining point in a s ## Frontmatter -- [ ] Frontmatter matches [plan frontmatter](${CLAUDE_PLUGIN_ROOT}/docs/template_plan_frontmatter.md). +- [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. - [ ] `sp` equals the sum of per-task SP across milestones. ## Content @@ -79,7 +79,7 @@ Name sections to update (new CLI in the `CLI` section, new inlining point in a s - [ ] DoD bullets are verifiable by invocation + output diff. - [ ] Every task lists exact files. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` invocation. +- [ ] Every milestone has a `Verify` invocation scoped to what it changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. - [ ] Each milestone executable from a fresh session with only the plan as context. ## I/O contract diff --git a/docs/plan_templates/documentation.md b/docs/plan_templates/documentation.md index abeeb456..013c6e90 100644 --- a/docs/plan_templates/documentation.md +++ b/docs/plan_templates/documentation.md @@ -39,7 +39,7 @@ Order pages so each milestone produces something reviewable in isolation. A typi **Goal**: one sentence — what page(s) or pipeline component lands. -**Verify**: build/serve the site locally and load the affected pages in a browser; check cross-links resolve; on CI changes, push a branch and confirm the workflow runs green. +**Verify**: build/serve the site locally and load the pages this milestone changed; check their cross-links resolve. Whole-repo gates — a strict full-site build, a pushed branch confirming the CI workflow green, an aggregate `ci` target — run once in Final Verification, never per milestone. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| @@ -75,7 +75,7 @@ Name sections to update (e.g. add `documentation/` to the layout section, distin ## Frontmatter -- [ ] Frontmatter matches [plan frontmatter](${CLAUDE_PLUGIN_ROOT}/docs/template_plan_frontmatter.md). +- [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. - [ ] `sp` equals the sum of per-task SP across milestones. ## Content @@ -85,7 +85,7 @@ Name sections to update (e.g. add `documentation/` to the layout section, distin - [ ] Each page is a milestone task or grouped with siblings under one milestone — no orphan pages. - [ ] DoD bullets are verifiable by loading the rendered page or running the build. - [ ] Every task lists exact file paths. -- [ ] Every milestone has a `Verify` step that includes a build or local-serve check. +- [ ] Every milestone has a `Verify` step that includes a build or local-serve check of the pages it changed — no whole-repo gate (strict full-site build, CI-workflow run, aggregate `ci` target); those belong to Final Verification. - [ ] Each milestone executable from a fresh session with only the plan as context. ## Documentation hygiene diff --git a/docs/plan_templates/frontend.md b/docs/plan_templates/frontend.md index 838ab099..a75f7a9d 100644 --- a/docs/plan_templates/frontend.md +++ b/docs/plan_templates/frontend.md @@ -25,7 +25,7 @@ How the new UI fits the component tree, state flow, and data-loading boundaries. **Goal**: one sentence — what changes in the UI after this milestone. -**Verify**: exact commands (typecheck, tests, visual check) or observable outcomes. +**Verify**: exact commands (scoped tests, a component's typecheck, visual check) or observable outcomes, limited to what this milestone changed. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| @@ -72,7 +72,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat ## Frontmatter -- [ ] Frontmatter matches [plan frontmatter](${CLAUDE_PLUGIN_ROOT}/docs/template_plan_frontmatter.md). +- [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. - [ ] `sp` equals the sum of per-task SP across milestones. ## Content @@ -81,7 +81,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat - [ ] DoD bullets are observable in the browser or a test runner. - [ ] Every task lists exact files. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` step. +- [ ] Every milestone has a `Verify` step scoped to what it changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. - [ ] Each milestone executable from a fresh session with only the plan as context. ## Anti-patterns (must be absent) diff --git a/docs/template_plan_frontmatter.md b/docs/template_plan_frontmatter.md deleted file mode 100644 index 33892295..00000000 --- a/docs/template_plan_frontmatter.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: {{Descriptive Title}} -type: feature | bug | refactoring -status: framing # the owning playbook's run status — written by `booping playbook-transition`, never by hand -sp: {{total}} -split_from: null # sibling stubs only: path to the primary plan this was split from -created: YYYY-MM-DD HH:MM # when this file was first written — the grooming run's clock, to the minute -planned: null # date keys — owned by the run machines' edge hooks, same YYYY-MM-DD HH:MM shape -started: null # set by the develop run machine when the sprint starts -completed: null # set by the develop run machine when the sprint ends -code_reviews: null # list of code-review artifact paths, appended by the code-review playbook -sessions: [] # Claude Code session ids, appended by the groom and develop edge hooks -retro: null # path to retrospective file, set by the retro playbook -goal: null # success | partial | fail — set by the retro playbook -summary: "" # one-line plan intent for search + plan listings (≤ ~120 chars) -commit: null # repo HEAD, snapshotted by the develop run machine at sprint entry ---- - diff --git a/playbooks/_partials/plan_frontmatter.md b/playbooks/_partials/plan_frontmatter.md deleted file mode 100644 index fa90dc1f..00000000 --- a/playbooks/_partials/plan_frontmatter.md +++ /dev/null @@ -1,18 +0,0 @@ -{% from "_partials/timestamps.md" import human_ts -%} -{% raw %}--- -title: {Descriptive Title} -type: feature | bug | refactoring -status: framing # the run machine's status — written by `booping playbook-transition`, never by hand -sp: {total} # sprint total, summed from milestone totals -split_from: null # sibling stubs only: path to the primary plan this was split from -{% endraw %}created: {{ human_ts }}{% raw %} # when the plan directory was created — the run's clock, to the minute -planned: null # date keys — owned by the run machine's edge hooks, same shape as `created` -started: null -completed: null -code_reviews: null # list of code-review artifact paths, appended by the code-review playbook -sessions: [] # Claude Code session ids, appended by the groom and develop edge hooks -retro: null -goal: null -summary: "" # one-line plan intent for search + plan listings (≤ ~120 chars) -commit: null ----{% endraw %} diff --git a/playbooks/_partials/plan_structure.md b/playbooks/_partials/plan_structure.md deleted file mode 100644 index 4892459a..00000000 --- a/playbooks/_partials/plan_structure.md +++ /dev/null @@ -1,33 +0,0 @@ -## Plan Structure - -The plan is the run's `index.md`: frontmatter, then the title, then the body. - -### Frontmatter - -```yaml -{% include "_partials/plan_frontmatter.md" %} -``` - -`sp` and `summary` are yours to write. `title` and `type` are intake's — correct them only where -the design changed them. `status` and every date and outcome key belong to the run machine and its hooks: whatever intake -left `null` stays `null`. - -### Title - -One H1 matching `title:`, and the only H1 in the file. - -### Body + Quality Checklist - -Each plan template is one file with two top-level sections: - -- `# Plan Body` — the structure the plan is written against, section for section, in its order. - None dropped, none extra. -- `# Quality Checklist` — walked item by item against the plan as written, before the plan is - returned. An unsatisfied item is fixed, not reported as satisfied. - -Read the chosen file before drafting: neither section can be guessed from its catalogue line. - -When no entry fits, author one at `{project}/plan_templates/{name}.md` first, then draft against -it — frontmatter (`name`, `description`) plus both top-level sections, generic for its surface -class: placeholders throughout, no path, milestone or story-point value from this run baked in. -Never draft into a bad-fit template, never improvise a shape and name a template after it. diff --git a/playbooks/_partials/plan_templates.md b/playbooks/_partials/plan_templates.md index 566223c7..0ac611ac 100644 --- a/playbooks/_partials/plan_templates.md +++ b/playbooks/_partials/plan_templates.md @@ -12,3 +12,23 @@ most of the milestones land on. A near miss loses on that surface, not on taste. {%- else %} _No plan templates found — author one before drafting._ {%- endif %} +Each template is one file with two top-level sections: + +- `# Plan Body` — the structure the plan is written against, section for section, in its order. + None dropped, none extra. +- `# Quality Checklist` — walked item by item against the plan as written, before the plan is + returned. An unsatisfied item is fixed, not reported as satisfied. + +Read the chosen file before drafting: neither section can be guessed from its catalogue line. + +When no entry fits, author one at `{project}/plan_templates/{name}.md` first, then draft against +it — frontmatter (`name`, `description`) plus both top-level sections, generic for its surface +class: placeholders throughout, no path, milestone or story-point value from this run baked in. +Never draft into a bad-fit template, never improvise a shape and name a template after it. + +The plan is the run's `index.md`, already carrying its frontmatter and title — the body goes under +them. `sp` and `summary` are yours; write them with: + +``` +booping frontmatter-update {plan}/index.md sp={total} summary="{one line}" +``` diff --git a/playbooks/groom/draft-plan/opus-5.md b/playbooks/groom/draft-plan/opus-5.md index ac668321..db261507 100644 --- a/playbooks/groom/draft-plan/opus-5.md +++ b/playbooks/groom/draft-plan/opus-5.md @@ -6,8 +6,7 @@ conversation already carries — the blast-radius map and the external ground th - **Draft design with the user**: architecture, pattern choices, data / API / config surface changes, open trade-offs. Iterate until aligned before writing. - **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) - whose name + description matches the work, then produce the plan against its `# Plan Body` — see - [Plan Structure](#plan-structure). + whose name + description matches the work, then produce the plan against its `# Plan Body`. - **Write `summary`**: set the `summary:` frontmatter to a single line of plain plan intent — ≤ ~120 chars / ~20 words, no prose, no trailing period needed. It feeds search and the `sprints.md` snapshot. @@ -22,8 +21,6 @@ conversation already carries — the blast-radius map and the external ground th may decline and keep one plan. - User approval is **explicit** — "looks good" is enough; silence is not. -{% include "_partials/plan_structure.md" %} - {% include "_partials/plan_templates.md" %} {% include "_partials/sprint_planning.md" %} \ No newline at end of file From 8efa59de2280885290a631a5a452e30585c149f6 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:06:26 +0700 Subject: [PATCH 08/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index 767b83d2..6bf3eb1d 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -156,7 +156,7 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo --- -### M4: Retire the frontmatter duplicates — 5 SP | pending +### M4: Retire the frontmatter duplicates — 5 SP | done **Goal**: the plan frontmatter shape exists only in `core.groom_playbook.scaffold`, and draft-plan writes its keys through `booping frontmatter-update`. @@ -164,22 +164,22 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 4.1 | Delete `_partials/plan_frontmatter.md` and `_partials/plan_structure.md`, folding the surviving template mechanics — the two top-level template sections, and authoring a new template when none fits — into `_partials/plan_templates.md`, which also names the `frontmatter-update` invocation that writes the drafter's keys | `playbooks/_partials/plan_frontmatter.md`, `playbooks/_partials/plan_structure.md`, `playbooks/_partials/plan_templates.md`, `playbooks/groom/draft-plan/opus-5.md` | 3 | pending | -| 4.2 | Delete `docs/template_plan_frontmatter.md` and repoint the Quality Checklist line in all five plan templates at the observable property instead of the deleted file | `docs/template_plan_frontmatter.md`, `docs/plan_templates/backend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md`, `docs/plan_templates/frontend.md` | 2 | pending | +| 4.1 | Delete `_partials/plan_frontmatter.md` and `_partials/plan_structure.md`, folding the surviving template mechanics — the two top-level template sections, and authoring a new template when none fits — into `_partials/plan_templates.md`, which also names the `frontmatter-update` invocation that writes the drafter's keys | `playbooks/_partials/plan_frontmatter.md`, `playbooks/_partials/plan_structure.md`, `playbooks/_partials/plan_templates.md`, `playbooks/groom/draft-plan/opus-5.md` | 3 | done | +| 4.2 | Delete `docs/template_plan_frontmatter.md` and repoint the Quality Checklist line in all five plan templates at the observable property instead of the deleted file | `docs/template_plan_frontmatter.md`, `docs/plan_templates/backend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md`, `docs/plan_templates/frontend.md` | 2 | done | #### Task 4.1 DoD -- [ ] `playbooks/_partials/plan_frontmatter.md` and `playbooks/_partials/plan_structure.md` are deleted, and `draft-plan/opus-5.md` includes only `_partials/plan_templates.md`. -- [ ] `plan_templates.md` carries the `# Plan Body` / `# Quality Checklist` mechanics and the author-a-new-template rule, unchanged in substance. -- [ ] `plan_templates.md` states that `sp` and `summary` are the drafter's and are written with `booping frontmatter-update {plan}/index.md sp=… summary="…"`. -- [ ] No partial contains a frontmatter yaml block or an H1-matching-title rule. -- [ ] `bin/booping render-playbook groom` exits 0 and its Draft Plan section renders without a template error. +- [x] `playbooks/_partials/plan_frontmatter.md` and `playbooks/_partials/plan_structure.md` are deleted, and `draft-plan/opus-5.md` includes only `_partials/plan_templates.md`. +- [x] `plan_templates.md` carries the `# Plan Body` / `# Quality Checklist` mechanics and the author-a-new-template rule, unchanged in substance. +- [x] `plan_templates.md` states that `sp` and `summary` are the drafter's and are written with `booping frontmatter-update {plan}/index.md sp=… summary="…"`. +- [x] No partial contains a frontmatter yaml block or an H1-matching-title rule. +- [x] `bin/booping render-playbook groom` exits 0 and its Draft Plan section renders without a template error. #### Task 4.2 DoD -- [ ] `docs/template_plan_frontmatter.md` is deleted. -- [ ] No file references `template_plan_frontmatter`. -- [ ] Each plan template's frontmatter checklist item asserts a property that can be checked against the plan itself. +- [x] `docs/template_plan_frontmatter.md` is deleted. +- [x] No file references `template_plan_frontmatter`. +- [x] Each plan template's frontmatter checklist item asserts a property that can be checked against the plan itself. --- From 81a5f1c7bf558a06a2409382b9f797fb111419ae Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:08:24 +0700 Subject: [PATCH 09/44] =?UTF-8?q?docs(booping):=20scaffold-seeded=20plan?= =?UTF-8?q?=20creation=20=E2=80=94=20three=20scaffold=20trees=20and=20the?= =?UTF-8?q?=20shared=20stdout=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- documentation/project_config.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/documentation/project_config.md b/documentation/project_config.md index 12929876..91814a60 100644 --- a/documentation/project_config.md +++ b/documentation/project_config.md @@ -16,7 +16,7 @@ Inside `core` there is one placement rule: | owned by exactly one playbook | `core.{name}_playbook.…` | | shared by the development loop or several playbooks | `core.…` directly | -`{name}` is the playbook's name with `-` replaced by `_` — `groom` → `core.groom_playbook`, `code-review` → `core.code_review_playbook`. The rule applies literally, including to the two scaffold trees. +`{name}` is the playbook's name with `-` replaced by `_` — `groom` → `core.groom_playbook`, `code-review` → `core.code_review_playbook`. The rule applies literally, including to the three scaffold trees. **`core` is the worked example your own playbooks copy.** A playbook you write declares its own namespace the same way and reads it with `{{ config.core.my_playbook.… }}` (or `{{ config.my_namespace.… }}` to sit outside `core` entirely). The placement rule travels with the copy: a key only your playbook reads sits in its `core.{name}_playbook` block; a key several of your playbooks share sits directly under `core`. @@ -306,7 +306,9 @@ Any mapping in the merged config — addressed by its dotted path, like a query bin/booping scaffold <config-path> <dest> [--force] [--set KEY=VALUE]... [--stub-macro DOTTED.PATH=LITERAL]... ``` -`<config-path>` is a dotted path into the merged config — the value there *is* the destination directory's contents, no wrapper key. `<dest>` is created with its parents when missing; a non-empty destination aborts unless you pass `--force`, which overwrites only the files the tree names and never deletes a directory. Exit 0 on success, 1 on a user error (unknown path, malformed tree, non-empty destination without `--force`, bad `--set`, Jinja error in seed content), 2 if a write fails at the OS level. The whole tree is rendered in memory first, so an error leaves the filesystem untouched. +`<config-path>` is a dotted path into the merged config — the value there *is* the destination directory's contents, no wrapper key. `<dest>` is created with its parents when missing, and a destination that already holds some of the tree's files is fine: **a file that exists is skipped**, reported as `skipped existing file {path}` and left byte-for-byte alone. `--force` turns that skip into an overwrite of the files the tree names; it never deletes a directory and never touches a path the tree does not name. Exit 0 on success, 1 on a user error (unknown path, malformed tree, bad `--set`, Jinja error in seed content), 2 if a write fails at the OS level. The whole tree is rendered in memory first, so an error leaves the filesystem untouched. + +**Stdout contract.** For every file the run actually wrote, scaffold prints a unified diff — `--- /dev/null` (a new file) or `--- {path}` (an overwrite), then `+++ {path}` and the hunks. A file whose rendered content matches what is already on disk, and a file skipped because it exists, produce no diff; created directories keep their one-line `created dir {path}` report, and the run still ends with the `scaffolded N paths — …` count line. `booping frontmatter-update` shares this contract: the diff of the change it made on stdout, nothing at all when the file did not change, its `updated {path}: {keys}` summary and any errors on stderr. Its arguments, flags and exit codes are unchanged; it writes scalars with their YAML type, so ints, floats, booleans and `null` land unquoted and everything else lands as a string. How a node is read: @@ -323,7 +325,7 @@ How a node is read: File content is Jinja-rendered, so `{{ config.… }}` and `{{ context.… }}` resolve. **`--set` here binds a bare variable** — `--set name=x` fills `{{ name }}` — unlike `booping render` and `booping render-playbook`, where `--set` merges into the config and you write `{{ config.name }}`. -Trees ride the same core → global → project merge as everything else here, so a global or project config can add its own tree or override one leaf of a shipped one. Two ship: +Trees ride the same core → global → project merge as everything else here, so a global or project config can add its own tree or override one leaf of a shipped one. Three ship: - **`core.playbook_authoring_playbook.scaffold`** — a playbook skeleton: `playbook.md` (identity frontmatter carrying the name you passed, plus a preamble stub), `playbook.yaml` (an empty `graph:`), and an empty `_references/`. @@ -334,6 +336,14 @@ Trees ride the same core → global → project merge as everything else here, s - **`core.setup_playbook.scaffold`** — the project vault: `plans/`, `retrospectives/`, `codereviews/`, `_lessons/`, `notes/`, plus the seeded `sprints.md` Obsidian Bases fence and a `.gitignore`. Takes no `--set` variables. +- **`core.groom_playbook.scaffold`** — one plan directory: `index.md` seeded with the plan's identity frontmatter (the sole definition of that frontmatter) plus the title as an `#` heading, and an empty `request.md`. Takes `--set title=` and `--set type=`. + + ``` + bin/booping scaffold core.groom_playbook.scaffold \ + ~/Claude/my-project/plans/202608091310_my-plan \ + --set title="My plan" --set type=feature + ``` + ## Review templates The code-review playbook picks its review template by name, and the template set layers across the same three levels as the config itself: the plugin ships a core set, the global level adds machine-wide templates at `<home_dir>/review_templates/`, and the Project Vault's `review_templates/` speaks for one project. Later levels override by name — a global or project template file named like a shipped one replaces it; a new name adds a template to the set. From 131ef1705d76b5432d709d9f67914f6a5b174058 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:08:24 +0700 Subject: [PATCH 10/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index 6bf3eb1d..beb362d4 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -183,7 +183,7 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo --- -### M5: Documentation — 2 SP | pending +### M5: Documentation — 2 SP | done **Goal**: the scaffold documentation describes three trees and the stdout contract both writers now share. @@ -191,15 +191,15 @@ Callers: `groom/intake` (new), `setup/setup-project`, `playbook-authoring/scaffo | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 5.1 | Update the scaffold section — the third tree and its `--set` variables, the unified-diff stdout contract shared with `frontmatter-update`, the per-file skip rule and what `--force` now means — and correct the "two scaffold trees" count | `documentation/project_config.md` | 2 | pending | +| 5.1 | Update the scaffold section — the third tree and its `--set` variables, the unified-diff stdout contract shared with `frontmatter-update`, the per-file skip rule and what `--force` now means — and correct the "two scaffold trees" count | `documentation/project_config.md` | 2 | done | #### Task 5.1 DoD -- [ ] `core.groom_playbook.scaffold` is listed beside the other two trees with its `--set` variables. -- [ ] The stdout contract is documented as a unified diff per changed file, silent on unchanged files. -- [ ] The skip-existing rule and `--force`'s meaning are stated. -- [ ] The sentence counting "the two scaffold trees" reads three. -- [ ] `just docs` builds without a broken-link warning on the edited page. +- [x] `core.groom_playbook.scaffold` is listed beside the other two trees with its `--set` variables. +- [x] The stdout contract is documented as a unified diff per changed file, silent on unchanged files. +- [x] The skip-existing rule and `--force`'s meaning are stated. +- [x] The sentence counting "the two scaffold trees" reads three. +- [x] `just docs` builds without a broken-link warning on the edited page. --- From 55ca5e2e53ac31ca4756259c37bfa0d342bfab1c Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:47:11 +0700 Subject: [PATCH 11/44] =?UTF-8?q?docs(booping):=20scaffold-seeded=20plan?= =?UTF-8?q?=20creation=20=E2=80=94=20changelog=20entry=20and=20the=20accep?= =?UTF-8?q?ted=20groom=20snapshot=20baseline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++ playbooks/groom/_reports/output.md | 101 ++++++++--------------------- 2 files changed, 36 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f687d634..89c7d6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Notable user-visible changes, newest first, in the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format; versions follow [semantic versioning](https://semver.org/). +## Unreleased + +### Changed + +- `booping scaffold` and `booping frontmatter-update` now answer with a unified diff of every file they changed, and say nothing about a file whose content did not move — a rendered prompt can act on the receipt instead of reading the file back. +- `booping scaffold` decides per file rather than per destination: an existing target is skipped and named, `--force` overwrites the files the tree names, and a destination that already holds part of the tree no longer aborts the run. +- `booping frontmatter-update` writes a scalar with its YAML type, so `sp=23` lands as an integer; a string whose plain form would reload as something else keeps its quotes. +- Groom seeds the plan directory from the new `core.groom_playbook.scaffold` tree, so a plan carries a real `created` and the repo's `commit` from the moment it exists, and the frontmatter shape lives in the config alone. + ## v1.0.0 — 2026-08-08 Everything booping does became a playbook. `/playbook` is now the only skill the plugin ships, procedures carry their own run state and resume across sessions, and retro and code review moved onto tracks of their own. diff --git a/playbooks/groom/_reports/output.md b/playbooks/groom/_reports/output.md index aadb18a1..ed071485 100644 --- a/playbooks/groom/_reports/output.md +++ b/playbooks/groom/_reports/output.md @@ -93,10 +93,9 @@ Check for context, in case a request is related to already existing plan. -## Task Types +## Task Type -Exactly one per plan. Pick the row the request meets, load its guidance before framing, and rule -the siblings out by name. +Exactly one per plan. Pick the row the request meets, load its guidance before framing and pick one. | type | fits when | guidance | | --- | --- | --- | @@ -105,30 +104,17 @@ the siblings out by name. | `refactoring` | Internal structure change with no user-visible behavior change. Needs current-vs-target design, migration steps, and a no-behavior-change DoD. | [guidance](${CLAUDE_PLUGIN_ROOT}/docs/task_refactoring.md) | -## The plan's identity frontmatter +## The plan directory -Create `index.md` carrying the shape below. `created` is the run's clock to the minute, not just -the date — copy the value verbatim. `status` is the run machine's and is written by -`booping playbook-transition`, never by hand. +Create it in one call, with `{plan-dir}` the preamble's `Plan dir:` line resolved against the +vault and `{title}` the plan's descriptive title: ---- -title: {Descriptive Title} -type: feature | bug | refactoring -status: framing # the run machine's status — written by `booping playbook-transition`, never by hand -sp: {total} # sprint total, summed from milestone totals -split_from: null # sibling stubs only: path to the primary plan this was split from -created: 1970-01-01 00:00 # when the plan directory was created — the run's clock, to the minute -planned: null # date keys — owned by the run machine's edge hooks, same shape as `created` -started: null -completed: null -code_reviews: null # list of code-review artifact paths, appended by the code-review playbook -sessions: [] # Claude Code session ids, appended by the groom and develop edge hooks -retro: null -goal: null -summary: "" # one-line plan intent for search + plan listings (≤ ~120 chars) -commit: null ---- +``` +booping scaffold core.groom_playbook.scaffold {plan-dir} --set title="{title}" --set type={type} +``` +The printed diff is the confirmation — do not read the created files back. A target reported as +already existing was not written; decide what that means for this run. ## The brief — written to `request.md`, posted in chat @@ -160,8 +146,7 @@ conversation already carries — the blast-radius map and the external ground th - **Draft design with the user**: architecture, pattern choices, data / API / config surface changes, open trade-offs. Iterate until aligned before writing. - **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) - whose name + description matches the work, then produce the plan against its `# Plan Body` — see - [Plan Structure](#plan-structure). + whose name + description matches the work, then produce the plan against its `# Plan Body`. - **Write `summary`**: set the `summary:` frontmatter to a single line of plain plan intent — ≤ ~120 chars / ~20 words, no prose, no trailing period needed. It feeds search and the `sprints.md` snapshot. @@ -176,44 +161,20 @@ conversation already carries — the blast-radius map and the external ground th may decline and keep one plan. - User approval is **explicit** — "looks good" is enough; silence is not. -## Plan Structure - -The plan is the run's `index.md`: frontmatter, then the title, then the body. - -### Frontmatter - -```yaml ---- -title: {Descriptive Title} -type: feature | bug | refactoring -status: framing # the run machine's status — written by `booping playbook-transition`, never by hand -sp: {total} # sprint total, summed from milestone totals -split_from: null # sibling stubs only: path to the primary plan this was split from -created: 1970-01-01 00:00 # when the plan directory was created — the run's clock, to the minute -planned: null # date keys — owned by the run machine's edge hooks, same shape as `created` -started: null -completed: null -code_reviews: null # list of code-review artifact paths, appended by the code-review playbook -sessions: [] # Claude Code session ids, appended by the groom and develop edge hooks -retro: null -goal: null -summary: "" # one-line plan intent for search + plan listings (≤ ~120 chars) -commit: null ---- - -``` - -`sp` and `summary` are yours to write. `title` and `type` are intake's — correct them only where -the design changed them. `status` and every date and outcome key belong to the run machine and its hooks: whatever intake -left `null` stays `null`. - -### Title +## Available plan templates -One H1 matching `title:`, and the only H1 in the file. +Pick the entry whose name and description match the **dominant surface** of the work — the surface +most of the milestones land on. A near miss loses on that surface, not on taste. -### Body + Quality Checklist +| Name | Source | Description | Read from | +| --- | --- | --- | --- | +| `backend` | core | Backend feature work — APIs, data models, services, migrations, background jobs, protocols. Stack-agnostic. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/backend.md` | +| `claude-skill` | core | Authoring or refactoring a Claude Code skill — skill bodies, partials, config schema, rendered outputs, skill-level agents. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/claude_skill.md` | +| `cli` | core | CLI tool work — argument parsing, subcommands, I/O, error handling, exit codes. Standalone scripts or larger CLI suites. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/cli.md` | +| `documentation` | core | Authoring or restructuring user-facing documentation — multi-page sites, READMEs, cross-linked guides, with optional static-site build pipeline (MkDocs, Jekyll, Docusaurus, etc.). | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/documentation.md` | +| `frontend` | core | Frontend feature work — UI components, state, routing, styling, accessibility. Stack-agnostic (React, Svelte, Leptos, Vue, vanilla). | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/frontend.md` | -Each plan template is one file with two top-level sections: +Each template is one file with two top-level sections: - `# Plan Body` — the structure the plan is written against, section for section, in its order. None dropped, none extra. @@ -227,20 +188,12 @@ it — frontmatter (`name`, `description`) plus both top-level sections, generic class: placeholders throughout, no path, milestone or story-point value from this run baked in. Never draft into a bad-fit template, never improvise a shape and name a template after it. +The plan is the run's `index.md`, already carrying its frontmatter and title — the body goes under +them. `sp` and `summary` are yours; write them with: -## Available plan templates - -Pick the entry whose name and description match the **dominant surface** of the work — the surface -most of the milestones land on. A near miss loses on that surface, not on taste. - -| Name | Source | Description | Read from | -| --- | --- | --- | --- | -| `backend` | core | Backend feature work — APIs, data models, services, migrations, background jobs, protocols. Stack-agnostic. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/backend.md` | -| `claude-skill` | core | Authoring or refactoring a Claude Code skill — skill bodies, partials, config schema, rendered outputs, skill-level agents. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/claude_skill.md` | -| `cli` | core | CLI tool work — argument parsing, subcommands, I/O, error handling, exit codes. Standalone scripts or larger CLI suites. | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/cli.md` | -| `documentation` | core | Authoring or restructuring user-facing documentation — multi-page sites, READMEs, cross-linked guides, with optional static-site build pipeline (MkDocs, Jekyll, Docusaurus, etc.). | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/documentation.md` | -| `frontend` | core | Frontend feature work — UI components, state, routing, styling, accessibility. Stack-agnostic (React, Svelte, Leptos, Vue, vanilla). | `${CLAUDE_PLUGIN_ROOT}/docs/plan_templates/frontend.md` | - +``` +booping frontmatter-update {plan}/index.md sp={total} summary="{one line}" +``` ## Sprint planning From 19d5902118c76e2ba590f5ecb5663b38edc3a6e4 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:47:22 +0700 Subject: [PATCH 12/44] =?UTF-8?q?develop:=20202608091310=5Fscaffold-seeded?= =?UTF-8?q?-plan-creation=20=E2=86=92=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index beb362d4..0b12b299 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -1,13 +1,13 @@ --- title: Scaffold-seeded plan creation and a scaffold receipt contract type: feature -status: in-progress +status: done sp: 24 related_to: null created: 2026-08-09 13:10 planned: null started: 2026-08-09 13:46 -completed: null +completed: 2026-08-09 14:47 code_reviews: [] sessions: - b1cf1fe7-f86c-4335-9434-c95e92d29a8c @@ -19,6 +19,13 @@ summary: groom seeds index.md via booping scaffold; scaffold and frontmatter-update both answer with a unified diff commit: 1bcc6229c7e8e013baec69c28747135663eb23e0 reviewed_at: 2026-08-09 13:44 +metrics_active_minutes: 60 +metrics_models: +- claude-opus-5 +metrics_tokens_input: 1467 +metrics_tokens_output: 207632 +metrics_tokens_cache_creation: 1098675 +metrics_tokens_cache_read: 29169559 --- # Scaffold-seeded plan creation and a scaffold receipt contract From 5ad3934eb82021a5cf4fbff383ec0c4a7767d099 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Sun, 9 Aug 2026 14:50:20 +0700 Subject: [PATCH 13/44] =?UTF-8?q?chore(booping):=20bump=20plugin=20version?= =?UTF-8?q?=201.0.0=20=E2=86=92=201.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 95e0e665..614783b7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "booping", - "version": "1.0.0", + "version": "1.0.1", "description": "Per-project grooming, implementation, retrospective, and lessons workflow with mandatory sub-agent delegation", "author": { "name": "Anton" From 9b09a762d7ee59aecda3769e2a9d2fccd512d7d1 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:04:42 +0700 Subject: [PATCH 14/44] fix(booping): harden the plan scaffold tree against an omitted --set `type:` rendered raw, so a missing `--set type=` produced a YAML null with no error and the run continued with a plan carrying no task type. It now goes through the same `default('') | tojson` seam `title:` already used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- src/config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.yaml b/src/config.yaml index 64cfcc95..977c3585 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -92,14 +92,14 @@ core: cross_review_agent: null # Scaffold tree materialised by # `booping scaffold core.groom_playbook.scaffold {vault}/plans/<slug> --set title=<title> --set type=<type>`. - # Sole definition of a plan's identity frontmatter. `title` goes through - # `tojson` because seed content is raw text with no YAML emitter behind it, so - # a colon or a quote in the title would otherwise break the document. + # Sole definition of a plan's identity frontmatter. The `--set` variables go + # through `tojson` because seed content is raw text with no YAML emitter behind + # it, so a colon, a quote or an omitted value would otherwise break the document. scaffold: index.md: | --- title: {{ title | tojson }} - type: {{ type }} + type: {{ (type | default('')) | tojson }} status: framing sp: null related_to: null From 1aff179c06b347aee3171326eb869c15c914c50c Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:04:50 +0700 Subject: [PATCH 15/44] test(booping): parametrize the frontmatter-update and scaffold receipt suites Scalar typing, frontmatter-update diff receipts and scaffold stdout receipts were one function per case. Each block is now a single parametrized test over an input/expected table, so a new case is a row. Assertions got stronger, not weaker: scalar rows check the reloaded value's exact type, and the append and removal rows assert the full `--- / +++ / @@` header. `import yaml as pyyaml` moves to module level; the eight function-local copies are gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../tests/commands/frontmatter_update_test.py | 172 ++++++++--------- .../tests/commands/scaffold_test.py | 175 ++++++++++-------- 2 files changed, 178 insertions(+), 169 deletions(-) diff --git a/booping-python/tests/commands/frontmatter_update_test.py b/booping-python/tests/commands/frontmatter_update_test.py index ceb17008..0b90381f 100644 --- a/booping-python/tests/commands/frontmatter_update_test.py +++ b/booping-python/tests/commands/frontmatter_update_test.py @@ -7,6 +7,7 @@ from typing import Any import pytest +import yaml as pyyaml from booping import macros from booping.commands import frontmatter_update as fu_cmd @@ -195,50 +196,38 @@ def _write(self, tmp_path: Path, pair: str) -> str: _, fm_text, _ = _split_result(plan.read_text()) return fm_text - def test_integer_lands_unquoted(self, tmp_path: Path) -> None: - assert "sp: 23\n" in self._write(tmp_path, "sp=23") - - def test_integer_round_trips_as_an_integer(self, tmp_path: Path) -> None: - import yaml as pyyaml - - assert pyyaml.safe_load(self._write(tmp_path, "sp=23"))["sp"] == 23 - - def test_float_lands_unquoted(self, tmp_path: Path) -> None: - assert "ratio: 1.5\n" in self._write(tmp_path, "ratio=1.5") - - def test_null_lands_as_yaml_null(self, tmp_path: Path) -> None: - assert "retro: null\n" in self._write(tmp_path, "retro=null") - - def test_boolean_lands_unquoted(self, tmp_path: Path) -> None: - assert "blocked: true\n" in self._write(tmp_path, "blocked=true") - - def test_partly_numeric_value_stays_a_string(self, tmp_path: Path) -> None: - import yaml as pyyaml - - fm_text = self._write(tmp_path, "summary=23 things") - assert "summary: 23 things\n" in fm_text - assert pyyaml.safe_load(fm_text)["summary"] == "23 things" - - def test_yaml_1_1_boolean_word_keeps_its_quotes(self, tmp_path: Path) -> None: - import yaml as pyyaml + @pytest.mark.parametrize( + ("pair", "expected_line", "expected_value"), + [ + ("sp=23", "sp: 23\n", 23), + ("ratio=1.5", "ratio: 1.5\n", 1.5), + ("retro=null", "retro: null\n", None), + ("blocked=true", "blocked: true\n", True), + ("summary=23 things", "summary: 23 things\n", "23 things"), + ("summary=yes", "summary: 'yes'\n", "yes"), + ], + ids=["integer", "float", "null", "boolean", "partly-numeric", "yaml-1-1-boolean-word"], + ) + def test_scalar_lands_typed( + self, tmp_path: Path, pair: str, expected_line: str, expected_value: object + ) -> None: + fm_text = self._write(tmp_path, pair) - fm_text = self._write(tmp_path, "summary=yes") - assert "summary: 'yes'\n" in fm_text - assert pyyaml.safe_load(fm_text)["summary"] == "yes" + assert expected_line in fm_text + key = pair.split("=", 1)[0] + loaded = pyyaml.safe_load(fm_text)[key] + assert loaded == expected_value + assert type(loaded) is type(expected_value) @pytest.mark.parametrize( "value", ["@lead", "*star", "&anchor", "!bang", "%pct", "`tick", "a: b", " padded "], ) def test_syntax_sensitive_string_keeps_its_quotes(self, tmp_path: Path, value: str) -> None: - import yaml as pyyaml - fm_text = self._write(tmp_path, f"summary={value}") assert pyyaml.safe_load(fm_text)["summary"] == value def test_macro_rendered_date_stays_a_string(self, tmp_path: Path) -> None: - import yaml as pyyaml - plan = _make_plan(tmp_path, "title: Foo") fu_cmd._run(_ns(plan=plan, pairs=[f"completed={NOW_EXPR}"])) # type: ignore[reportPrivateUsage] @@ -257,8 +246,6 @@ def test_sets_planned_with_date_macro(self, tmp_path: Path) -> None: text = plan.read_text() assert "planned:" in text _, fm_text, _ = _split_result(text) - import yaml as pyyaml - fm = pyyaml.safe_load(fm_text) datetime.strptime(fm["planned"], "%Y-%m-%d %H:%M") # noqa: DTZ007 @@ -281,8 +268,6 @@ def test_sets_commit_with_the_git_macro( text = plan.read_text() assert "commit:" in text _, fm_text, _ = _split_result(text) - import yaml as pyyaml - fm = pyyaml.safe_load(fm_text) assert len(fm["commit"]) == 40 @@ -294,8 +279,6 @@ def test_sets_created_with_date_macro(self, tmp_path: Path) -> None: text = plan.read_text() assert "created:" in text _, fm_text, _ = _split_result(text) - import yaml as pyyaml - fm = pyyaml.safe_load(fm_text) assert datetime.strptime(str(fm["created"]), "%Y-%m-%d") # noqa: DTZ007 @@ -399,8 +382,6 @@ def test_append_creates_list_on_null_key(self, tmp_path: Path) -> None: fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] - import yaml as pyyaml - _, fm_text, _ = _split_result(plan.read_text()) assert pyyaml.safe_load(fm_text)["sessions"] == ["abc-123"] @@ -409,8 +390,6 @@ def test_append_creates_list_on_absent_key(self, tmp_path: Path) -> None: fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] - import yaml as pyyaml - _, fm_text, _ = _split_result(plan.read_text()) assert pyyaml.safe_load(fm_text)["sessions"] == ["abc-123"] @@ -419,8 +398,6 @@ def test_append_extends_existing_list(self, tmp_path: Path) -> None: fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=def-456"])) # type: ignore[reportPrivateUsage] - import yaml as pyyaml - _, fm_text, _ = _split_result(plan.read_text()) assert pyyaml.safe_load(fm_text)["sessions"] == ["abc-123", "def-456"] @@ -430,8 +407,6 @@ def test_append_is_idempotent(self, tmp_path: Path) -> None: fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] - import yaml as pyyaml - _, fm_text, _ = _split_result(plan.read_text()) assert pyyaml.safe_load(fm_text)["sessions"] == ["abc-123"] @@ -457,8 +432,6 @@ def test_append_alongside_pairs_and_removals(self, tmp_path: Path) -> None: ) ) - import yaml as pyyaml - _, fm_text, _ = _split_result(plan.read_text()) fm = pyyaml.safe_load(fm_text) assert fm["status"] == "in-progress" @@ -475,60 +448,67 @@ def test_append_only_still_reports_nothing_to_do_when_empty( assert excinfo.value.code == 1 assert "nothing to do" in capsys.readouterr().err + @pytest.mark.parametrize( + ("frontmatter", "args", "expected", "unexpected"), + [ + ( + "title: Foo\nstatus: backlog", + {"pairs": ["status=in-progress"]}, + ["-status: backlog", "+status: in-progress"], + ["+title: Foo"], + ), + ( + "title: Foo\nsessions:\n- abc-123", + {"pairs": [], "appends": ["sessions=def-456"]}, + ["+- def-456"], + [], + ), + ( + "title: Foo\nbusiness_goal: x\nstatus: backlog", + {"pairs": [], "removals": ["business_goal"]}, + ["-business_goal: x"], + [], + ), + ("title: Foo\nstatus: backlog", {"pairs": ["status=backlog"]}, [], []), + ( + "title: Foo\nsessions:\n- abc-123", + {"pairs": [], "appends": ["sessions=abc-123"]}, + [], + [], + ), + ], + ids=[ + "changed-value", + "append", + "removal", + "unchanged-value-prints-nothing", + "idempotent-append-prints-nothing", + ], + ) def test_prints_a_unified_diff_of_the_change( - self, tmp_path: Path, capsys: pytest.CaptureFixture[str] + self, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + frontmatter: str, + args: dict[str, list[str]], + expected: list[str], + unexpected: list[str], ) -> None: - plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") + plan = _make_plan(tmp_path, frontmatter) - fu_cmd._run(_ns(plan=plan, pairs=["status=in-progress"])) # type: ignore[reportPrivateUsage] + fu_cmd._run(_ns(plan=plan, **args)) # type: ignore[reportPrivateUsage] + + out = capsys.readouterr().out + if not expected: + assert out == "" + return - lines = capsys.readouterr().out.splitlines() + lines = out.splitlines() assert lines[0] == f"--- {plan}" assert lines[1] == f"+++ {plan}" assert lines[2].startswith("@@") - assert "-status: backlog" in lines - assert "+status: in-progress" in lines - assert "+title: Foo" not in lines - - def test_unchanged_value_prints_nothing( - self, tmp_path: Path, capsys: pytest.CaptureFixture[str] - ) -> None: - plan = _make_plan(tmp_path, "title: Foo\nstatus: backlog") - - fu_cmd._run(_ns(plan=plan, pairs=["status=backlog"])) # type: ignore[reportPrivateUsage] - - assert capsys.readouterr().out == "" - - def test_idempotent_append_prints_nothing( - self, tmp_path: Path, capsys: pytest.CaptureFixture[str] - ) -> None: - plan = _make_plan(tmp_path, "title: Foo\nsessions:\n- abc-123") - - fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=abc-123"])) # type: ignore[reportPrivateUsage] - - assert capsys.readouterr().out == "" - - def test_append_prints_the_added_line( - self, tmp_path: Path, capsys: pytest.CaptureFixture[str] - ) -> None: - plan = _make_plan(tmp_path, "title: Foo\nsessions:\n- abc-123") - - fu_cmd._run(_ns(plan=plan, pairs=[], appends=["sessions=def-456"])) # type: ignore[reportPrivateUsage] - - lines = capsys.readouterr().out.splitlines() - assert lines[0] == f"--- {plan}" - assert "+- def-456" in lines - - def test_removal_prints_the_dropped_line( - self, tmp_path: Path, capsys: pytest.CaptureFixture[str] - ) -> None: - plan = _make_plan(tmp_path, "title: Foo\nbusiness_goal: x\nstatus: backlog") - - fu_cmd._run(_ns(plan=plan, pairs=[], removals=["business_goal"])) # type: ignore[reportPrivateUsage] - - lines = capsys.readouterr().out.splitlines() - assert lines[0] == f"--- {plan}" - assert "-business_goal: x" in lines + assert all(line in lines for line in expected) + assert all(line not in lines for line in unexpected) def test_summary_stays_on_stderr( self, tmp_path: Path, capsys: pytest.CaptureFixture[str] diff --git a/booping-python/tests/commands/scaffold_test.py b/booping-python/tests/commands/scaffold_test.py index 00017e44..944cb20e 100644 --- a/booping-python/tests/commands/scaffold_test.py +++ b/booping-python/tests/commands/scaffold_test.py @@ -2,6 +2,9 @@ import subprocess from pathlib import Path +from typing import NamedTuple + +import pytest PLUGIN_ROOT = Path(__file__).resolve().parents[3] BOOPING_BIN = PLUGIN_ROOT / "bin" / "booping" @@ -209,89 +212,115 @@ def test_malformed_set_pair_exits_1_matching_render_message( # --- Task 2.3: report, exit codes, logging --------------------------------- -def test_report_diffs_dirs_and_summary( - tmp_path: Path, isolated_xdg_config_home: Path -) -> None: - dest = tmp_path / "out" - dest.mkdir() - (dest / "README.md").write_text("old\n") - result = _scaffold(tmp_path, isolated_xdg_config_home, "demo", str(dest), "--force") - assert result.returncode == 0, result.stderr - lines = result.stdout.splitlines() - assert f"--- {dest / 'README.md'}" in lines - assert f"+++ {dest / 'README.md'}" in lines - assert "-old" in lines - assert "+hello " in lines - assert "--- /dev/null" in lines - assert f"+++ {dest / 'src' / 'main.py'}" in lines - assert "+print(1)" in lines - assert f"created dir {dest / '_references'}" in lines - assert lines[-1] == ( - "scaffolded 4 paths — 2 dirs created, 1 files created, 1 files overwritten" - ) - assert result.stderr == "" +class ReceiptCase(NamedTuple): + """One scaffold receipt: destination state and flags in, stdout lines out. + + Expected lines carry `{dest}`, formatted with the run's destination. + """ + + existing: dict[str, str] + args: tuple[str, ...] + head: tuple[str, ...] + contains: tuple[str, ...] + summary: str + tree: str = TREE + line_count: int | None = None + + +RECEIPT_CASES = [ + pytest.param( + ReceiptCase( + existing={"README.md": "old\n"}, + args=("--force",), + head=(), + contains=( + "--- {dest}/README.md", + "+++ {dest}/README.md", + "-old", + "+hello ", + "--- /dev/null", + "+++ {dest}/src/main.py", + "+print(1)", + "created dir {dest}/_references", + ), + summary="scaffolded 4 paths — 2 dirs created, 1 files created, 1 files overwritten", + ), + id="diffs-dirs-and-summary", + ), + pytest.param( + ReceiptCase( + existing={}, + args=(), + head=( + "--- /dev/null", + "+++ {dest}/a.txt", + "@@ -0,0 +1,2 @@", + "+one", + "+two", + ), + contains=(), + summary="scaffolded 1 paths — 0 dirs created, 1 files created, 0 files overwritten", + tree='demo:\n a.txt: "one\\ntwo\\n"\n', + ), + id="new-file-is-all-additions-from-dev-null", + ), + pytest.param( + ReceiptCase( + existing={"a.txt": "one\nold\nthree\n"}, + args=("--force",), + head=( + "--- {dest}/a.txt", + "+++ {dest}/a.txt", + "@@ -1,3 +1,3 @@", + " one", + "-old", + "+two", + " three", + ), + contains=(), + summary="scaffolded 1 paths — 0 dirs created, 0 files created, 1 files overwritten", + tree='demo:\n a.txt: "one\\ntwo\\nthree\\n"\n', + ), + id="overwrite-carries-context-lines", + ), + pytest.param( + ReceiptCase( + existing={"a.txt": "same\n"}, + args=("--force",), + head=(), + contains=(), + summary="scaffolded 0 paths — 0 dirs created, 0 files created, 0 files overwritten", + tree='demo:\n a.txt: "same\\n"\n', + line_count=1, + ), + id="unchanged-file-prints-no-diff", + ), +] -def test_new_file_diff_is_all_additions_from_dev_null( - tmp_path: Path, isolated_xdg_config_home: Path +@pytest.mark.parametrize("case", RECEIPT_CASES) +def test_receipt_stdout( + case: ReceiptCase, tmp_path: Path, isolated_xdg_config_home: Path ) -> None: dest = tmp_path / "out" dest.mkdir() - result = _scaffold( - tmp_path, - isolated_xdg_config_home, - "demo", - str(dest), - tree='demo:\n a.txt: "one\\ntwo\\n"\n', - ) - assert result.returncode == 0, result.stderr - lines = result.stdout.splitlines() - assert lines[0] == "--- /dev/null" - assert lines[1] == f"+++ {dest / 'a.txt'}" - assert lines[2] == "@@ -0,0 +1,2 @@" - assert lines[3:5] == ["+one", "+two"] - + for rel, body in case.existing.items(): + (dest / rel).write_text(body) -def test_overwrite_diff_carries_context_lines( - tmp_path: Path, isolated_xdg_config_home: Path -) -> None: - dest = tmp_path / "out" - dest.mkdir() - (dest / "a.txt").write_text("one\nold\nthree\n") result = _scaffold( - tmp_path, - isolated_xdg_config_home, - "demo", - str(dest), - "--force", - tree='demo:\n a.txt: "one\\ntwo\\nthree\\n"\n', + tmp_path, isolated_xdg_config_home, "demo", str(dest), *case.args, tree=case.tree ) assert result.returncode == 0, result.stderr - lines = result.stdout.splitlines() - assert lines[0] == f"--- {dest / 'a.txt'}" - assert lines[1] == f"+++ {dest / 'a.txt'}" - assert lines[2] == "@@ -1,3 +1,3 @@" - assert lines[3:7] == [" one", "-old", "+two", " three"] - + assert result.stderr == "" -def test_unchanged_file_prints_no_diff( - tmp_path: Path, isolated_xdg_config_home: Path -) -> None: - dest = tmp_path / "out" - dest.mkdir() - (dest / "a.txt").write_text("same\n") - result = _scaffold( - tmp_path, - isolated_xdg_config_home, - "demo", - str(dest), - "--force", - tree='demo:\n a.txt: "same\\n"\n', - ) - assert result.returncode == 0, result.stderr - assert result.stdout.splitlines() == [ - "scaffolded 0 paths — 0 dirs created, 0 files created, 0 files overwritten" - ] + lines = result.stdout.splitlines() + head = [line.format(dest=dest) for line in case.head] + assert lines[: len(head)] == head + for line in case.contains: + assert line.format(dest=dest) in lines + assert lines[-1] == case.summary + if case.line_count is not None: + assert len(lines) == case.line_count def test_unknown_config_path_exits_1( From 8bed56c6d6b81a937ee52d9d40c35c9c3e285b69 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:04:59 +0700 Subject: [PATCH 16/44] docs(booping): code-review run for scaffold-seeded plan creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review artifact, the plan's `code_reviews:` back-link, and two lessons the run produced — top-level imports over lazy ones, and code-only review scope. The back-link is hand-corrected: `frontmatter-update --append` into an empty inline `[]` list emitted the old `[]` as the first element. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- vault/_lessons/0014_avoid-lazy-imports.md | 12 +++ vault/_lessons/0015_review-only-code-files.md | 12 +++ .../202608091453.md | 101 ++++++++++++++++++ .../index.md | 3 +- 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 vault/_lessons/0014_avoid-lazy-imports.md create mode 100644 vault/_lessons/0015_review-only-code-files.md create mode 100644 vault/codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md diff --git a/vault/_lessons/0014_avoid-lazy-imports.md b/vault/_lessons/0014_avoid-lazy-imports.md new file mode 100644 index 00000000..1ac0c9cb --- /dev/null +++ b/vault/_lessons/0014_avoid-lazy-imports.md @@ -0,0 +1,12 @@ +--- +id: 14 +title: Use top-level imports; resort to lazy imports only when there is no other way +targets: + - agent:booping-developer +retro: null +created: 2026-08-10 +--- + +Place imports at the top of the module. A lazy (function-local) import is an anti-pattern reached for only when a genuine constraint — a circular dependency that cannot be restructured, an optional heavy dependency — leaves no alternative. + +**Example**: Importing a module inside a function body to dodge a circular import hides the real coupling; restructure the modules instead, and only fall back to the local import when no restructuring works. diff --git a/vault/_lessons/0015_review-only-code-files.md b/vault/_lessons/0015_review-only-code-files.md new file mode 100644 index 00000000..bf9ae747 --- /dev/null +++ b/vault/_lessons/0015_review-only-code-files.md @@ -0,0 +1,12 @@ +--- +id: 15 +title: Review only code files — skip documentation, plans, and other prose +targets: + - code-review +retro: null +created: 2026-08-10 +--- + +Scope the review to code files only. Documentation, plans, and other prose in the diff are out of scope — do not surface findings on them. + +**Example**: A sprint diff touching `src/` modules plus a plan's `index.md` and a README gets findings only on the `src/` files; the markdown changes pass through unreviewed. diff --git a/vault/codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md b/vault/codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md new file mode 100644 index 00000000..0c5d6ed8 --- /dev/null +++ b/vault/codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md @@ -0,0 +1,101 @@ +--- +plan: plans/202608091310_scaffold-seeded-plan-creation/index.md +scope: diff 1bcc622..HEAD on feat/scaffold-seeded-plans — 12 commits, 22 files +created: 2026-08-09 14:53 +status: done +agents: + review: a5e8fb5dafbba5346 +reviewed_at: 2026-08-09 19:52 +--- + +# Code review — Scaffold-seeded plan creation and a scaffold receipt contract + +## Scope + +`plans/202608091310_scaffold-seeded-plan-creation/index.md` — the plan this session's develop run delivered. Baseline `1bcc6229c7e8e013baec69c28747135663eb23e0`. + +Diff range `1bcc622..HEAD` on `feat/scaffold-seeded-plans` — 12 commits, 22 files changed, +735/−198. + +| Changed file | +/− | +| --- | --- | +| `vault/plans/202608091310_scaffold-seeded-plan-creation/index.md` | +248/−0 | +| `booping-python/tests/commands/frontmatter_update_test.py` | +126/−0 | +| `booping-python/tests/commands/scaffold_test.py` | +93/−10 | +| `booping-python/src/booping/commands/frontmatter_update.py` | +55/−2 | +| `vault/plans/202608091310_scaffold-seeded-plan-creation/request.md` | +49/−0 | +| `playbooks/groom/_reports/output.md` | +27/−74 | +| `src/config.yaml` | +26/−0 | +| `booping-python/src/booping/commands/scaffold.py` | +21/−13 | +| `booping-python/src/booping/utils.py` | +21/−0 | +| `playbooks/_partials/plan_templates.md` | +20/−0 | +| … 12 more: `documentation/project_config.md`, `playbooks/groom/intake/fable-5.md`, `CHANGELOG.md`, `playbooks/groom/draft-plan/opus-5.md`, the five `docs/plan_templates/*.md`, and the deleted `docs/template_plan_frontmatter.md`, `playbooks/_partials/plan_frontmatter.md`, `playbooks/_partials/plan_structure.md` | + +Working tree is clean — nothing uncommitted sits outside the range. + +Artifact `codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md` — opened at `in-agent-review`. + +## Findings + +**BLOCKER (5)** +- `booping-python/src/booping/commands/scaffold.py:86` — `_render_seed` catches only `TemplateError`, but `MacroError` is a plain `Exception`: `except TemplateError as exc:` → also catch `MacroError` (`except (TemplateError, MacroError) as exc:`) so a failing `macro()` exits 1 with an `error:` line instead of a traceback — `core.groom_playbook.scaffold` is the first tree to call `macro()` (`src/config.yaml:106,114`); `render.py:101` already catches it. Cites plan §I/O contract, `documentation/project_config.md:309`. +- `src/config.yaml:101` — `title: {{ title | tojson }}` raises `TypeError: Object of type LenientUndefined is not JSON serializable` when `--set title=` is omitted, which is not a `TemplateError` → traceback instead of exit 1: `{{ (title | default('')) | tojson }}`. Cites I/O-contract / exit codes. +- `booping-python/src/booping/utils.py:23` — `return "\n".join(lines)` returns an empty receipt for a real write when `splitlines()` shows no difference: a new empty file (exactly `request.md`, `src/config.yaml:118`, written on every groom run) and a `--force` overwrite differing only in a trailing newline. Emit a `--- /dev/null` / `+++ {path}` header when `unified_diff` yields nothing. Cites plan Task 1.1 DoD + I/O contract; `intake/fable-5.md:26` tells the model the diff replaces reading the file back. +- `playbooks/setup/setup-project/opus-5.md:20` — stale precondition left behind by M1.2: "A non-empty destination exits 1; ask before re-running it with `--force`" describes a guard the CLI no longer has → replace with the per-file rule (existing file skipped and named, `--force` overwrites, exit 0 either way). Cites lesson `0005_stale-reference-cleanup-in-same-sprint`. +- `vault/plans/202608091310_scaffold-seeded-plan-creation/index.md:225` — all seven Final Verification boxes unticked while `status: done` and every task DoD `[x]`; `playbooks/develop/playbook.md:12` requires Final Verification green at the exit gate, and the three preceding closed plans tick them → tick each against evidence or record the failure, especially items 4 and 6 (the only end-to-end exercise of the new per-file write semantics and of a real groom-created `commit:`). + +**SUGGESTION (6)** +- `booping-python/src/booping/commands/scaffold.py:122` — `previous = write.path.read_text(encoding="utf-8") if existed else None` is a new read of arbitrary on-disk content; `UnicodeDecodeError` is a `ValueError`, so `_run`'s `except OSError` (line 174) misses it → add `UnicodeDecodeError` to that handler so a non-UTF-8 target exits 2. Cites security checklist (output/error handling), python checklist (specific exception types). +- `booping-python/src/booping/commands/frontmatter_update.py:172` — `--append` values bypass `coerce_scalar` / `_quote_if_ambiguous`, so Task 2.2's round-trip guarantee holds for `key=value` but not for the list path (`--append flags=yes` reloads as a boolean) → route appends through the same coercion, or call `_quote_if_ambiguous` on them. Cites coding-architecture — responsibility ownership. +- `src/config.yaml:102` — `type: {{ type }}` is raw where `title` is hardened; an omitted `--set type=` renders a YAML null with no error → `{{ (type | default('')) | tojson }}`. Cites Task 3.1 DoD (valid YAML from `--set` input). +- `src/config.yaml:105` — the tree is declared the sole definition of plan identity frontmatter but emits `related_to:` (read by nothing) while dropping `split_from:` (still documented at `documentation/vault.md:19`) and `goal:` (still read by `playbooks/retro/gather-feedback/base.md:50`, `playbooks/retro/intake/base.md:17`); M5 updated only `project_config.md` → reconcile the key names and state whether `goal:` is intentionally absent. Cites stale reference. +- `playbooks/groom/intake/fable-5.md:14` — edit outside M3.2's stated file scope that drops the "rule the siblings out by name" instruction and says "pick" twice → restore the rule-out sentence or take the edit deliberately. Cites plan-intent match. +- `CHANGELOG.md:9` — three of four bullets describe internals a user never invokes, framed for prompt authors ("a rendered prompt can act on the receipt") → collapse into one user-facing line; the receipt contract belongs in `documentation/project_config.md`. Cites lesson `0006_audience-scoped-content` as a repo convention. + +**NIT (2)** +- `booping-python/tests/commands/frontmatter_update_test.py:200` — `import yaml as pyyaml` repeated inside six test methods → one module-level import. Cites python checklist — idiomatic Python. +- `booping-python/src/booping/commands/frontmatter_update.py:174` — `plan_path.read_text()` is locale-dependent while the sibling receipt path (`scaffold.py:122`) pins UTF-8 → `read_text(encoding="utf-8")`. Cites python checklist — idiomatic Python. + +## Verdict + +Collected through the Plannotator review surface (lesson `0012`). A seeded finding the human did not carry back is dismissed in that surface; five findings are the human's own, raised on the test files. + +- `src/config.yaml:102` — SUGGESTION · approved, seeded fix kept verbatim: `type: {{ (type | default('')) | tojson }}` +- `booping-python/src/booping/commands/scaffold.py:86` — BLOCKER · dismissed in the review surface +- `src/config.yaml:101` — BLOCKER · dismissed in the review surface +- `booping-python/src/booping/utils.py:23` — BLOCKER · dismissed in the review surface +- `playbooks/setup/setup-project/opus-5.md:20` — BLOCKER · dismissed in the review surface +- `vault/plans/202608091310_scaffold-seeded-plan-creation/index.md:225` — BLOCKER · dismissed in the review surface +- `booping-python/src/booping/commands/scaffold.py:122` — SUGGESTION · dismissed in the review surface +- `booping-python/src/booping/commands/frontmatter_update.py:172` — SUGGESTION · dismissed in the review surface +- `src/config.yaml:105` — SUGGESTION · dismissed in the review surface +- `playbooks/groom/intake/fable-5.md:14` — SUGGESTION · dismissed in the review surface +- `CHANGELOG.md:9` — SUGGESTION · dismissed in the review surface +- `booping-python/tests/commands/frontmatter_update_test.py:200` — NIT · dismissed as seeded, re-raised by the human at `:202` (below) +- `booping-python/src/booping/commands/frontmatter_update.py:174` — NIT · dismissed in the review surface + +Raised by the human in the review surface — all approved by definition: + +- `booping-python/tests/commands/frontmatter_update_test.py:202` — "Anti-pattern - don't use lazy loading if it works without it." +- `booping-python/tests/commands/frontmatter_update_test.py:206` — "Looks like this tests might be parametrized rather than a separate tests. You can use input/expected result pairs here" +- `booping-python/tests/commands/frontmatter_update_test.py:522` — "can this be also parametrized tests in a manner like plan_content, expected multi-line diff? It looks easier to support/extend." +- `booping-python/tests/commands/scaffold_test.py:83` — "just to confirm, it's tempdir or regular dirs? https://docs.python.org/3/library/tempfile.html" — a question, answered in the closing report; no edit +- `booping-python/tests/commands/scaffold_test.py:221` — "also feel like parametrized tests" + +## Resolution + +**Applied here (1)** +- `src/config.yaml:102` — SUGGESTION · `type:` now renders through `(type | default('')) | tojson`; the comment above the tree updated from "`title` goes through `tojson`" to the `--set` variables generally + +**Delegated to `booping:booping-developer` (4)** +- `booping-python/tests/commands/frontmatter_update_test.py:202` — `import yaml as pyyaml` hoisted to module level; all 8 function-level copies removed +- `booping-python/tests/commands/frontmatter_update_test.py:206` — `TestScalarTyping`'s seven scalar tests collapsed into a parametrized `test_scalar_lands_typed` over `(pair, expected_line, expected_value)`, both original assertions kept per row plus a `type(loaded) is type(expected_value)` guard +- `booping-python/tests/commands/frontmatter_update_test.py:522` — five diff-receipt tests folded into a parametrized `test_prints_a_unified_diff_of_the_change` over `(frontmatter, args, expected, unexpected)`; append and removal rows now assert the full `--- / +++ / @@` header +- `booping-python/tests/commands/scaffold_test.py:221` — four stdout-receipt tests collapsed into a parametrized `test_receipt_stdout` over a `ReceiptCase` table; the summary line carried as its own field + +**Answered, no edit (1)** +- `booping-python/tests/commands/scaffold_test.py:83` — `tmp_path` is pytest's builtin fixture: a real per-test directory on disk under the system temp base (`pytest-of-$USER/pytest-N/…`), which is what these tests need since the CLI runs as a subprocess. Nothing to change. + +**Dropped in the review surface (12)** +- The five `BLOCKER`s and six `SUGGESTION`s of the seeded set, plus the `frontmatter_update.py:174` `NIT`, were dismissed in Plannotator and are not carried into the code. + +Verification after the fixes: `just lint` clean, `just typecheck` 0 errors, `just pytest` 824 passed, `just snapshots` exit 0 — the `src/config.yaml` edit drifts no committed report. diff --git a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md index 0b12b299..4b4bf92f 100644 --- a/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md +++ b/vault/plans/202608091310_scaffold-seeded-plan-creation/index.md @@ -8,7 +8,8 @@ created: 2026-08-09 13:10 planned: null started: 2026-08-09 13:46 completed: 2026-08-09 14:47 -code_reviews: [] +code_reviews: + - codereviews/202608091310_scaffold-seeded-plan-creation/202608091453.md sessions: - b1cf1fe7-f86c-4335-9434-c95e92d29a8c - b74396a9-b99a-442d-9be4-9c58e6c11d5c From 01166768323963a33706694e7bcd8e2f13809d80 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:11:31 +0700 Subject: [PATCH 17/44] fix(code-review): close-code-review mishandled an inline code_reviews list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exit hook parses plan frontmatter with a regex — it is stdlib-only by design — and its inline-value branch knew only `null` and `~`. A plan carrying `code_reviews: []` had that `[]` inserted as the list's first element; a populated `[a]` would have landed as one element literally named `[a]`. `_inline_items()` now reads the flow form: `[]` yields nothing, `[a, b]` yields its elements, `null`/`~` mean unset, anything else is a lone scalar. The hook has no tests, so the new module drives it as a subprocess the way `playbook-transition` does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../tests/scripts/close_code_review_test.py | 124 ++++++++++++++++++ .../code-review/_scripts/close-code-review | 26 +++- 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 booping-python/tests/scripts/close_code_review_test.py diff --git a/booping-python/tests/scripts/close_code_review_test.py b/booping-python/tests/scripts/close_code_review_test.py new file mode 100644 index 00000000..c246b49b --- /dev/null +++ b/booping-python/tests/scripts/close_code_review_test.py @@ -0,0 +1,124 @@ +"""Tests for the `code-review` playbook's exit hook script. + +The script is plugin-shipped and stdlib-only, so it is exercised as a subprocess the +way `booping playbook-transition` runs it: cwd and `BOOPING_WORKDIR` at the vault root, +`BOOPING_ARTIFACT` at the resolved review. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from typing import NamedTuple + +import pytest + +SCRIPT = ( + Path(__file__).resolve().parents[3] + / "playbooks" + / "code-review" + / "_scripts" + / "close-code-review" +) + +REVIEW_REL = "codereviews/20260809_plan/202608091453.md" +PLAN_REL = "plans/20260809_plan/index.md" + + +def _vault(tmp_path: Path, plan_frontmatter: str) -> Path: + vault = tmp_path / "vault" + review = vault / REVIEW_REL + review.parent.mkdir(parents=True) + review.write_text(f"---\nplan: {PLAN_REL}\nstatus: human-review\n---\n\n# Review\n") + + plan = vault / PLAN_REL + plan.parent.mkdir(parents=True) + plan.write_text(f"---\ntitle: Foo\n{plan_frontmatter}\nstatus: done\n---\n\n# Foo\n") + return vault + + +def _run(vault: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(SCRIPT)], + cwd=vault, + env={ + "PATH": "/usr/bin:/bin", + "BOOPING_WORKDIR": str(vault), + "BOOPING_ARTIFACT": str(vault / REVIEW_REL), + }, + capture_output=True, + text=True, + ) + + +def _code_reviews(plan: Path) -> list[str]: + lines = plan.read_text().partition("\n---\n")[0].splitlines() + start = next(i for i, line in enumerate(lines) if line.startswith("code_reviews:")) + end = start + 1 + while end < len(lines) and lines[end].lstrip().startswith("- "): + end += 1 + return [line.lstrip()[2:].strip() for line in lines[start + 1 : end]] + + +class Case(NamedTuple): + frontmatter: str + expected: list[str] + + +CASES = { + # the regression: an empty inline list is empty, not an element named "[]" + "empty-inline-list": Case("code_reviews: []", [REVIEW_REL]), + "null-scalar": Case("code_reviews: null", [REVIEW_REL]), + "tilde-scalar": Case("code_reviews: ~", [REVIEW_REL]), + "key-absent": Case("sp: 4", [REVIEW_REL]), + "populated-block-list": Case( + "code_reviews:\n - codereviews/20260809_plan/202608081200.md", + ["codereviews/20260809_plan/202608081200.md", REVIEW_REL], + ), + "populated-inline-list": Case( + "code_reviews: [codereviews/20260809_plan/202608081200.md]", + ["codereviews/20260809_plan/202608081200.md", REVIEW_REL], + ), +} + + +@pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) +def test_review_is_appended_as_its_own_element(tmp_path: Path, case: Case) -> None: + vault = _vault(tmp_path, case.frontmatter) + + result = _run(vault) + + assert result.returncode == 0, result.stderr + assert _code_reviews(vault / PLAN_REL) == case.expected + + +def test_an_already_linked_review_is_not_duplicated(tmp_path: Path) -> None: + vault = _vault(tmp_path, f"code_reviews:\n - {REVIEW_REL}") + + result = _run(vault) + + assert result.returncode == 0, result.stderr + assert _code_reviews(vault / PLAN_REL) == [REVIEW_REL] + assert "already linked to" in result.stdout + + +def test_the_body_and_the_other_keys_survive(tmp_path: Path) -> None: + vault = _vault(tmp_path, "code_reviews: []") + + assert _run(vault).returncode == 0 + + text = (vault / PLAN_REL).read_text() + assert text.startswith("---\ntitle: Foo\n") + assert "\nstatus: done\n" in text + assert text.endswith("---\n\n# Foo\n") + + +def test_a_missing_plan_aborts_the_transition(tmp_path: Path) -> None: + vault = _vault(tmp_path, "code_reviews: []") + (vault / PLAN_REL).unlink() + + result = _run(vault) + + assert result.returncode != 0 + assert "plan referenced by `plan:` not found" in result.stderr diff --git a/playbooks/code-review/_scripts/close-code-review b/playbooks/code-review/_scripts/close-code-review index a001a2c8..5e1cb9df 100755 --- a/playbooks/code-review/_scripts/close-code-review +++ b/playbooks/code-review/_scripts/close-code-review @@ -30,6 +30,21 @@ def _clean(value: str) -> str: return value.split(" #")[0].strip().strip("\"'") +def _inline_items(value: str) -> list[str]: + """The items a key already carries on its own line. + + A flow list yields its elements, so an empty `[]` yields nothing rather than an + item literally named `[]`. `null` and `~` mean the key is unset; anything else is + a lone scalar item. + """ + value = _clean(value) + if value.startswith("[") and value.endswith("]"): + return [_clean(item) for item in value[1:-1].split(",") if item.strip()] + if value in ("", "null", "~"): + return [] + return [value] + + def read_plan_key(path: Path) -> str | None: match = FRONTMATTER.match(path.read_text()) if not match: @@ -47,8 +62,8 @@ def read_plan_key(path: Path) -> str | None: def append_review(plan: Path, review: str) -> bool: """Add `review` to the plan's `code_reviews:` block list, creating the key when it - is absent and replacing a `null` scalar with a one-element list. Returns whether - the plan was rewritten.""" + is absent and replacing a `null` scalar or an empty `[]` with a one-element list. + Returns whether the plan was rewritten.""" text = plan.read_text() if not text.startswith("---\n"): sys.exit(f"plan has no frontmatter: {plan}") @@ -68,13 +83,10 @@ def append_review(plan: Path, review: str) -> bool: end = start + 1 while end < len(lines) and lines[end].lstrip().startswith("- "): end += 1 - existing = [_clean(line.lstrip()[2:]) for line in lines[start + 1 : end]] + existing = _inline_items(lines[start].partition(":")[2]) + existing += [_clean(line.lstrip()[2:]) for line in lines[start + 1 : end]] if review in existing: return False - - scalar = _clean(lines[start].partition(":")[2]) - if scalar and scalar not in ("null", "~"): - existing.insert(0, scalar) rewritten = ["code_reviews:"] + [f" - {item}" for item in [*existing, review]] lines[start:end] = rewritten plan.write_text("\n".join(lines) + sep + body) From 40049f5d68a84ac6dafcf1c0e42171566dbb0098 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:20:55 +0700 Subject: [PATCH 18/44] docs(booping): fold the lazy-import lesson into a code-style lesson `0014` broadens from lazy imports alone to the project's code style practices, picking up the parametrized-tests preference the same review raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- vault/_lessons/0014_avoid-lazy-imports.md | 12 ------------ vault/_lessons/0014_code-style-guide.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 vault/_lessons/0014_avoid-lazy-imports.md create mode 100644 vault/_lessons/0014_code-style-guide.md diff --git a/vault/_lessons/0014_avoid-lazy-imports.md b/vault/_lessons/0014_avoid-lazy-imports.md deleted file mode 100644 index 1ac0c9cb..00000000 --- a/vault/_lessons/0014_avoid-lazy-imports.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: 14 -title: Use top-level imports; resort to lazy imports only when there is no other way -targets: - - agent:booping-developer -retro: null -created: 2026-08-10 ---- - -Place imports at the top of the module. A lazy (function-local) import is an anti-pattern reached for only when a genuine constraint — a circular dependency that cannot be restructured, an optional heavy dependency — leaves no alternative. - -**Example**: Importing a module inside a function body to dodge a circular import hides the real coupling; restructure the modules instead, and only fall back to the local import when no restructuring works. diff --git a/vault/_lessons/0014_code-style-guide.md b/vault/_lessons/0014_code-style-guide.md new file mode 100644 index 00000000..a29472f9 --- /dev/null +++ b/vault/_lessons/0014_code-style-guide.md @@ -0,0 +1,13 @@ +--- +id: 14 +title: Follow the project code style practices +targets: + - agent:booping-developer +retro: null +created: 2026-08-10 +--- + +Code style practices: + +1. Do not use lazy (function-local) imports in Python; top-level imports only, unless there is genuinely no other way. +2. Prefer parametrized tests when testing the same surface with different inputs and expected results. From dfcbe647decd23b8ba139f2acec1d0204c95cf07 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 12:29:54 +0700 Subject: [PATCH 19/44] docs: 202608101223-docs-refresh @ researching --- vault/docs/_runs/202608101223-docs-refresh.md | 80 +++++++++++++++++++ vault/docs/_specs/documented.md | 35 ++++++++ 2 files changed, 115 insertions(+) create mode 100644 vault/docs/_runs/202608101223-docs-refresh.md diff --git a/vault/docs/_runs/202608101223-docs-refresh.md b/vault/docs/_runs/202608101223-docs-refresh.md new file mode 100644 index 00000000..41db6e8b --- /dev/null +++ b/vault/docs/_runs/202608101223-docs-refresh.md @@ -0,0 +1,80 @@ +--- +title: Docs refresh +status: researching +started: 2026-08-10 12:26 +commit: 40049f5d68a84ac6dafcf1c0e42171566dbb0098 +agents.survey: ac07340b037418656 +scope_reviewed_at: 2026-08-10 12:29 +--- + +# Docs refresh + +## Spec set + +| File | State | Gap | +| --- | --- | --- | +| `_specs/index.md` | present | — | +| `_specs/roles.md` | present | — | +| `_specs/targets.md` | present | — | +| `_specs/features.md` | present | — | + +4 of 4 usable — no file needs the spec waves; `index.md`, `roles.md`, `targets.md` and `features.md` are all complete. + +## Undocumented work + +| Work item | Status | Landed | Headline | +| --- | --- | --- | --- | +| `plans/20260422-plans-as-data-refactor/index.md` | done | 2026-04-22 | Plans-as-data refactor | +| `plans/20260423-plugin-docs-hygiene-pass/index.md` | done | 2026-04-23 | Plugin docs hygiene pass | +| `plans/20260423-refactor-chat-skill-to-groom-pattern/index.md` | done | 2026-04-23 | Refactor /chat skill to groom pattern | +| `plans/20260423-refactor-develop-skill-to-groom-pattern/index.md` | done | 2026-04-23 | Refactor /develop skill to groom pattern | +| `plans/20260423-refactor-learn-skill/index.md` | done | 2026-04-23 | Refactor /learn skill to groom pattern with unified review table and debug mode | +| `plans/20260423-refactor-retro-skill-to-groom-pattern/index.md` | done | 2026-04-23 | Refactor /retro skill to groom pattern | +| `plans/20260424-refactor-install-help-skills-to-groom-shape/index.md` | done | 2026-04-24 | Refactor /install and /help to groom shape; excise vault CLAUDE.md across the plugin | +| `plans/20260425-migrate-develop-skill-to-template-pipeline/index.md` | done | 2026-04-25 | Migrate /develop skill to template pipeline | +| `plans/20260425-migrate-learn-skill-to-template-pipeline/index.md` | done | 2026-04-26 | Migrate /learn skill to template pipeline | +| `plans/20260425-migrate-retro-skill-to-template-pipeline/index.md` | done | 2026-04-26 | Migrate /retro skill to template pipeline | +| `plans/20260426-migrate-install-help-template-pipeline-cli/index.md` | done | 2026-04-26 | Migrate /install and /help to template pipeline (CLI-driven /help) | +| `plans/20260426-readme-and-claude-md-refresh/index.md` | done | 2026-04-26 | README & CLAUDE.md Refresh | +| `plans/20260427-migrate-chat-template-pipeline-and-prune-partials/index.md` | done | 2026-04-27 | Migrate /chat to template pipeline + prune orphaned docs/partial_*.md | +| `plans/20260429-jinja-rendered-plans-listing/index.md` | done | — | Render plans listing via Jinja templates | +| `plans/20260429-skill-runtime-template-rendering/index.md` | done | — | Skill runtime template rendering with project config overrides | +| `plans/20260430-add-code-review-skill/index.md` | done | — | Add /code-review skill with layered templates | +| `plans/20260430-plan-commit-drift-and-code-review-status/index.md` | done | 2026-04-30 | Plan-commit drift detection and code-review plan picker | +| `plans/20260430-src-files-build-pipeline/index.md` | done | 2026-04-30 | src/files build pipeline for skills and agents | +| `plans/20260430-user-facing-documentation-site/index.md` | done | — | User-facing documentation site | +| `plans/20260519-cli-agent-delegation/index.md` | done | — | External CLI Agent Delegation for /develop | +| `plans/20260520-log-all-booping-cli-calls/index.md` | done | 2026-06-10 | Log project-facing `booping` CLI invocations to `.booping.log` | +| `plans/20260530-namespace-internal-agents-prefix/index.md` | done | 2026-06-10 | Namespace internal agents with `booping:` prefix in delegation table | +| `plans/20260607-cli-agent-native-wrapper/index.md` | done | — | CLI agents as native wrapper agents + /compile skill | +| `plans/20260608-plannotator-code-review-surface/index.md` | done | 2026-06-10 | Plannotator-backed code-review surface (global, booping-decoupled) | +| `plans/20260610-simplify-cli-delegation-to-global-agent/index.md` | done | 2026-06-10 | Retire cli-agent wrapper subsystem; adopt the plannotator global-agent pattern | +| `plans/20260614-actualize-docs-site-and-readme/index.md` | done | 2026-06-14 | Actualize documentation site and README against v0.1.5 | +| `plans/20260615-deterministic-transition-hooks/index.md` | done | — | Deterministic plan transitions via config-wired hooks + Harel superstates | +| `plans/20260628-local-vault-directories/index.md` | done | 2026-06-28 | Local Vault Directories | +| `plans/20260629-install-stop-seeding-duplicate-extensions/index.md` | done | 2026-06-28 | Install stops seeding CLAUDE.md-duplicating extensions | +| `plans/20260709-benchmark-framework-core/index.md` | done | 2026-07-08 | Benchmark framework core (claude-booping-bench) + develop reference case | +| `plans/20260709-benchmark-judge-groom-case-reports/index.md` | done | 2026-07-09 | Bench scoring v2, judge kind, groom cases, compare reports | +| `plans/20260722-booping-global-config-home-dir/index.md` | done | 2026-07-22 | Booping Global Config + home_dir | +| `plans/20260722-jinja-playbooks-composed-step-rendering/index.md` | done | 2026-07-22 | Jinja Playbooks — Composed Step Rendering | +| `plans/20260722-playbooks-framework-pilot/index.md` | done | — | Playbooks Framework + Build-User-Stories Pilot | +| `plans/20260724-playbook-frontmatter-graph/index.md` | done | 2026-07-24 | Playbook Frontmatter Graph — Parallel Step Execution | +| `plans/202608072133_learn-targets-consolidation/index.md` | done | 2026-08-08 | Learn targets consolidation — extra-instructions retirement, code-review skill removal, skill:playbook lesson target | +| `plans/202608081300_session-time-metrics/index.md` | done | 2026-08-08 | Session active-time metrics — lead/cycle time from Claude Code session logs | +| `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | done | 2026-08-08 | Justfile script extraction and dead-code cleanup for release | +| `plans/202608081523_session-metrics-idle-and-tokens/index.md` | done | 2026-08-08 | Idle-aware active time and per-session token accounting | +| `plans/202608091310_scaffold-seeded-plan-creation/index.md` | done | 2026-08-09 | Scaffold-seeded plan creation and a scaffold receipt contract | + +40 items outside the ledger, landing 2026-04-22 → 2026-08-09 (six with no completion stamp); the ledger holds 18 rows, its most recent `plans/202608081156_code-review-track-split/index.md`. + +## Scope + +Confirmed 2026-08-10. Spec set current — no spec file refreshed; spec waves skipped. Everything landed before August 2026 is marked stale in `_specs/documented.md` — the docs process only covers work from August on. In scope, 5 delivered items: + +| Work item | Headline | +| --- | --- | +| `plans/202608072133_learn-targets-consolidation/index.md` | Learn targets consolidation — extra-instructions retirement, code-review skill removal, skill:playbook lesson target | +| `plans/202608081300_session-time-metrics/index.md` | Session active-time metrics — lead/cycle time from Claude Code session logs | +| `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | Justfile script extraction and dead-code cleanup for release | +| `plans/202608081523_session-metrics-idle-and-tokens/index.md` | Idle-aware active time and per-session token accounting | +| `plans/202608091310_scaffold-seeded-plan-creation/index.md` | Scaffold-seeded plan creation and a scaffold receipt contract | diff --git a/vault/docs/_specs/documented.md b/vault/docs/_specs/documented.md index 4b8d8e4d..2b51a9c3 100644 --- a/vault/docs/_specs/documented.md +++ b/vault/docs/_specs/documented.md @@ -22,3 +22,38 @@ One row per work item every one of whose changes is documented; the survey's und | `plans/202608061007_testing-ci-snapshots-mdcheck/index.md` | `_runs/202608081436-pr19-plans-docs.md` | | `plans/202608071455_retro-status-track-split/index.md` | `_runs/202608081436-pr19-plans-docs.md` | | `plans/202608081156_code-review-track-split/index.md` | `_runs/202608081436-pr19-plans-docs.md` | +| `plans/20260422-plans-as-data-refactor/index.md` | stale — pre-August, predates the docs process | +| `plans/20260423-plugin-docs-hygiene-pass/index.md` | stale — pre-August, predates the docs process | +| `plans/20260423-refactor-chat-skill-to-groom-pattern/index.md` | stale — pre-August, predates the docs process | +| `plans/20260423-refactor-develop-skill-to-groom-pattern/index.md` | stale — pre-August, predates the docs process | +| `plans/20260423-refactor-learn-skill/index.md` | stale — pre-August, predates the docs process | +| `plans/20260423-refactor-retro-skill-to-groom-pattern/index.md` | stale — pre-August, predates the docs process | +| `plans/20260424-refactor-install-help-skills-to-groom-shape/index.md` | stale — pre-August, predates the docs process | +| `plans/20260425-migrate-develop-skill-to-template-pipeline/index.md` | stale — pre-August, predates the docs process | +| `plans/20260425-migrate-learn-skill-to-template-pipeline/index.md` | stale — pre-August, predates the docs process | +| `plans/20260425-migrate-retro-skill-to-template-pipeline/index.md` | stale — pre-August, predates the docs process | +| `plans/20260426-migrate-install-help-template-pipeline-cli/index.md` | stale — pre-August, predates the docs process | +| `plans/20260426-readme-and-claude-md-refresh/index.md` | stale — pre-August, predates the docs process | +| `plans/20260427-migrate-chat-template-pipeline-and-prune-partials/index.md` | stale — pre-August, predates the docs process | +| `plans/20260429-jinja-rendered-plans-listing/index.md` | stale — pre-August, predates the docs process | +| `plans/20260429-skill-runtime-template-rendering/index.md` | stale — pre-August, predates the docs process | +| `plans/20260430-add-code-review-skill/index.md` | stale — pre-August, predates the docs process | +| `plans/20260430-plan-commit-drift-and-code-review-status/index.md` | stale — pre-August, predates the docs process | +| `plans/20260430-src-files-build-pipeline/index.md` | stale — pre-August, predates the docs process | +| `plans/20260430-user-facing-documentation-site/index.md` | stale — pre-August, predates the docs process | +| `plans/20260519-cli-agent-delegation/index.md` | stale — pre-August, predates the docs process | +| `plans/20260520-log-all-booping-cli-calls/index.md` | stale — pre-August, predates the docs process | +| `plans/20260530-namespace-internal-agents-prefix/index.md` | stale — pre-August, predates the docs process | +| `plans/20260607-cli-agent-native-wrapper/index.md` | stale — pre-August, predates the docs process | +| `plans/20260608-plannotator-code-review-surface/index.md` | stale — pre-August, predates the docs process | +| `plans/20260610-simplify-cli-delegation-to-global-agent/index.md` | stale — pre-August, predates the docs process | +| `plans/20260614-actualize-docs-site-and-readme/index.md` | stale — pre-August, predates the docs process | +| `plans/20260615-deterministic-transition-hooks/index.md` | stale — pre-August, predates the docs process | +| `plans/20260628-local-vault-directories/index.md` | stale — pre-August, predates the docs process | +| `plans/20260629-install-stop-seeding-duplicate-extensions/index.md` | stale — pre-August, predates the docs process | +| `plans/20260709-benchmark-framework-core/index.md` | stale — pre-August, predates the docs process | +| `plans/20260709-benchmark-judge-groom-case-reports/index.md` | stale — pre-August, predates the docs process | +| `plans/20260722-booping-global-config-home-dir/index.md` | stale — pre-August, predates the docs process | +| `plans/20260722-jinja-playbooks-composed-step-rendering/index.md` | stale — pre-August, predates the docs process | +| `plans/20260722-playbooks-framework-pilot/index.md` | stale — pre-August, predates the docs process | +| `plans/20260724-playbook-frontmatter-graph/index.md` | stale — pre-August, predates the docs process | From b3f061aa41425c5926b0c73089bc3d8ab1655bdc Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 13:43:25 +0700 Subject: [PATCH 20/44] docs: 202608101223-docs-refresh @ updating --- vault/docs/_runs/202608101223-docs-refresh.md | 56 ++++++++++++++++++- vault/docs/_specs/features.md | 13 ++--- vault/docs/_specs/index.md | 2 +- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/vault/docs/_runs/202608101223-docs-refresh.md b/vault/docs/_runs/202608101223-docs-refresh.md index 41db6e8b..8e98b2ff 100644 --- a/vault/docs/_runs/202608101223-docs-refresh.md +++ b/vault/docs/_runs/202608101223-docs-refresh.md @@ -1,10 +1,15 @@ --- title: Docs refresh -status: researching +status: updating started: 2026-08-10 12:26 commit: 40049f5d68a84ac6dafcf1c0e42171566dbb0098 agents.survey: ac07340b037418656 scope_reviewed_at: 2026-08-10 12:29 +agents.research: fan-out x5 +changes_reviewed_at: 2026-08-10 12:37 +agents.sync-specs: ad066f3e46928c014 +agents.targeting-reads: aecc5c3690b9a64d2 +targeting_reviewed_at: 2026-08-10 13:43 --- # Docs refresh @@ -78,3 +83,52 @@ Confirmed 2026-08-10. Spec set current — no spec file refreshed; spec waves sk | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | Justfile script extraction and dead-code cleanup for release | | `plans/202608081523_session-metrics-idle-and-tokens/index.md` | Idle-aware active time and per-session token accounting | | `plans/202608091310_scaffold-seeded-plan-creation/index.md` | Scaffold-seeded plan creation and a scaffold receipt contract | + +## Changes + +| # | Work item | Type | What changed | Spec-set effect | +| --- | --- | --- | --- | --- | +| C1 | `plans/202608072133_learn-targets-consolidation/index.md` | new feature | Lessons can target a skill directly via `skill:{name}`, so a targeted lesson renders inside a skill body (e.g. `/playbook`) the same way `agent:{id}` lessons already render inside an agent | `features.md`: extend Targeted lessons (group "Shaping booping to a project") to list `skill:{name}` alongside `agent:{id}` as a lesson target kind | +| C2 | `plans/202608072133_learn-targets-consolidation/index.md` | feature drop | Authoring skill or agent extension files under `_booping/` no longer has any effect; shaping a skill's or agent's behavior for a project is done only through targeted lessons | `features.md`: drop the skill/agent extra-instructions capability under "Shaping booping to a project" (superseded by Targeted lessons); `targets.md`: documentation/integrating-external-agents.md loses its extension-file wiring section, keeping only config registration | +| C3 | `plans/202608072133_learn-targets-consolidation/index.md` | vision shift | The plugin ships a single skill, `/playbook`; code review is reached only via `/playbook code-review`, there is no standalone `/code-review` skill | `index.md`: record convergence on the single-`/playbook`-skill shape; `features.md`: drop the code-review skill entry (the Code review playbook entry stays); `targets.md`: documentation/code_review.md becomes a playbook page, README's skill list drops `/code-review` | +| C4 | `plans/202608072133_learn-targets-consolidation/index.md` | refactoring | The CLI's invocation log lives at `{vault}/.booping.log`, at the vault root; a freshly scaffolded vault has no `_booping/` directory | `targets.md`: documentation/vault.md and README's vault-layout description move the log path to the vault root and drop `_booping/` | +| C5 | `plans/202608072133_learn-targets-consolidation/index.md` | new feature | Running `/playbook migrate` on an older vault converts existing `_booping/skill_*.md` and `agent_*.md` extension files into targeted lessons and removes the converted originals | `features.md`: add the extras-to-lessons conversion capability under group "Setup" (migrate feature) | +| C6 | `plans/202608081300_session-time-metrics/index.md` | new feature | Session metrics land: per-plan active work minutes and the models that ran them, computed from Claude Code session logs, auto-stamped on groom and develop transitions and surfaced via `sprints.md` and query columns | `features.md`: move "session active-time metrics" out of "Not features" into a feature row (group "The sprint loop"); `index.md`: drop the in-flight example from "Where it is heading" — delivered now | +| C7 | `plans/202608081300_session-time-metrics/index.md` | new feature | `frontmatter-update --append` adds list-append semantics (create-if-missing, dedup) for stamping keys such as `sessions:` without regex-editing YAML | `features.md`: add an `--append` capability bullet to the Run state machines row (group "Running procedures") | +| C8 | `plans/202608081300_session-time-metrics/index.md` | chore | Existing vaults gain `active_minutes` and `models` columns in `sprints.md` via a shipped migration; new vaults scaffold them from the start | none — covered generically by the existing Project Vault migrations feature row | +| C9 | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | refactoring | Dev tooling for snapshots and mdcheck lives in `scripts/` as Python instead of inline justfile bash; recipe names and observable behavior unchanged | none | +| C10 | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | refactoring | Eval and report scripts moved from `bin/` to `scripts/`; `bin/` holds only the product CLI `booping` | `features.md`: `bin/eval-*.sh` / report jq path mentions under "Not features" become `scripts/` | +| C11 | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | feature drop | The `booping-create-project` script is gone; project scaffolding is exclusively the setup playbook and `booping scaffold` | none — `features.md` already lists `booping-create-project` as retired under "Not features" | +| C12 | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | chore | The retired Gemini cross-validation caller and its templates are gone; quick start no longer lists a `GEMINI_API_KEY` prerequisite | `targets.md`: documentation/quick_start.md no longer names `GEMINI_API_KEY` as a setup prerequisite; `features.md`: drop the `bin/booping-external-llm-call` line under "Not features" | +| C13 | `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | chore | Stale report fossils and an unreferenced playbook-authoring lib file were deleted | none | +| C14 | `plans/202608081523_session-metrics-idle-and-tokens/index.md` | new feature | `booping session-stats` excludes time spent waiting on a human (question answers, rejected tool calls) from a plan's active-time total and adds per-session token accounting (input, output, cache-creation, cache-read) plus model ids, stamped as `metrics_*` frontmatter and surfaced as `sprints.md` columns; it fully replaces the short-lived `session-time` command as the one metrics surface | `features.md`: the session-metrics feature row (from C6) describes idle-aware active time plus per-session token accounting via `booping session-stats` | +| C15 | `plans/202608091310_scaffold-seeded-plan-creation/index.md` | new feature | `booping scaffold` and `booping frontmatter-update` report every write as a unified diff on stdout, silent when a file is unchanged, so a caller no longer reads a file back to see what landed | `features.md`: Layered configuration entry gains the shared stdout diff-receipt contract for `booping scaffold` / `booping frontmatter-update` | +| C16 | `plans/202608091310_scaffold-seeded-plan-creation/index.md` | refactoring | `booping scaffold` no longer errors when the destination already holds some of the tree's files; each existing target is skipped and reported individually unless `--force` overwrites it | `features.md`: Layered configuration scaffold semantics note the per-file skip-existing / `--force` rule replacing the old dest-not-empty error | +| C17 | `plans/202608091310_scaffold-seeded-plan-creation/index.md` | new feature | Groom creates a plan's directory with one `booping scaffold` call against a config-declared tree instead of hand-copying a frontmatter block, and the new `index.md` carries a real `commit:` (repo HEAD) from the moment the plan is created instead of staying null until a sprint starts | `features.md`: Groom entry updated — creates `plans/{slug}/index.md` via `booping scaffold`, commit stamped at creation | +| C18 | `plans/202608091310_scaffold-seeded-plan-creation/index.md` | chore | The standalone frontmatter-template fragment each plan template's Quality Checklist pointed to is gone; the checklist item asserts an observable frontmatter property instead of linking a doc file | `targets.md`: the `docs/` surface loses `template_plan_frontmatter.md`, no replacement needed | +| C19 | user request 2026-08-10 | vision shift | Playbooks are no longer an unstable work-in-progress: the framework is the plugin's converged core, and the docs site drops its instability disclaimers (`documentation/index.md` nav note, the warning admonition atop `documentation/playbook.md`) | none — `index.md` (briefing) already records the converged v1.0 shape | + +19 rows over 5 items plus 1 user-requested change — 2 vision shift, 7 new feature, 4 refactoring, 2 feature drop, 4 chore; `features.md` moved by 12 rows, `targets.md` by 5, `index.md` by 2, `roles.md` by none. + +## Targeting plan + +| Destination | Roles | Changes | Must say | Progress | +| --- | --- | --- | --- | --- | +| `README.md` | Users | C3, C4 | C3: the plugin ships one skill, `/playbook`; code review runs via `/playbook code-review` — any standalone `/code-review` skill mention goes. C4: the vault-layout narrative places the CLI log at the vault root (`.booping.log`) and carries no `_booping/` directory. | pending | +| `documentation/index.md` | Users | C19, C3 | C19: the Playbooks nav line loses "*Unstable — work in progress.*". C3: the home page presents `/playbook` as the single shipped skill, code review as one of its playbooks. | pending | +| `documentation/playbook.md` | Advanced users | C19, C1, C7 | C19: the "Unstable — work in progress" warning admonition is removed, no replacement. C1: lesson `targets:` kinds list `skill:{name}` beside `agent:{id}`, injecting into skill bodies. C7: the `frontmatter-update` hook/CLI documents `--append key=val` — list append, create-if-missing, dedup, scalar clash is an error. | pending | +| `documentation/code_review.md` | Users | C3 | The page teaches `/playbook code-review` as the only entry; any framing of a standalone `/code-review` skill is rewritten to the playbook invocation. | pending | +| `documentation/vault.md` | Users, Advanced users | C4, C2, C5, C6, C14 | C4: `.booping.log` sits at the vault root; the layout lists no `_booping/`. C2: project shaping is targeted lessons only — no skill/agent extension files. C5: `/playbook migrate` converts an older vault's extension files into targeted lessons and removes the originals. C6+C14: `sprints.md` carries `active_minutes`, `models` and token columns, fed from `metrics_*` plan frontmatter stamped by the sprint playbooks. | pending | +| `documentation/develop.md` | Users | C6, C14 | Session metrics: active work minutes and models are stamped on groom and develop transitions from Claude Code session logs; `booping session-stats` is the one metrics surface — idle-aware active time (human-wait excluded) plus per-session token accounting. | pending | +| `documentation/groom.md` | Users | C17 | Groom creates the plan directory with one `booping scaffold` call from a config-declared tree; the fresh `index.md` carries a real `commit:` (repo HEAD) from creation. | pending | +| `documentation/project_config.md` | Advanced users | C15, C16, C17 | C15: `booping scaffold` and `booping frontmatter-update` print a unified-diff receipt per write, silent when unchanged. C16: scaffold skips existing target files per-file and reports each, `--force` overwrites — the dest-not-empty error is gone. C17: the plan scaffold tree groom seeds from is a config-declared scaffold tree, named at its dotted path. | pending | + +## Not targeted + +- C8 (chore) — the `sprints.md` column migration ships and announces itself via the render-surface watermark warning; the columns themselves are documented under the vault.md row (C6+C14). +- C9 (refactoring) — internal dev-tooling move, no observable change; CLAUDE.md already reflects `scripts/`. +- C10 (refactoring) — `bin/` → `scripts/` path move; CLAUDE.md already names `scripts/` for the eval harness, no public surface lists those paths. +- C11 (feature drop) — `booping-create-project` was never documented on a current surface; docs already point at `/playbook setup` + `booping scaffold`. +- C12 (chore) — quick start and CLAUDE.md already carry no `GEMINI_API_KEY` or external-llm-call references (verified in research). +- C13 (chore) — deleted fossils, nothing documented anywhere. +- C18 (chore) — the fragment is deleted; the `docs/` surface carries no inventory naming it. diff --git a/vault/docs/_specs/features.md b/vault/docs/_specs/features.md index 24e34815..f7e751b0 100644 --- a/vault/docs/_specs/features.md +++ b/vault/docs/_specs/features.md @@ -12,8 +12,8 @@ shipped only, each line tagged with the roles it serves. Terms follow [glossary. | group | feature | capabilities | | -------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Running procedures** | Playbook driver — the `/playbook` skill that discovers a procedure and drives it to completion | - list the playbooks visible from the core, global and project levels, names unique across levels — a clash is flagged in the listing and blocks the run with a STOP #user<br>- select one by its trigger, or by name when the user names it #user<br>- drive steps in graph order, running each parallel wave as detached sub-agents #user<br>- nest a mapping node in `graph:` as a subgraph — own dependencies, inner graph, optional prose `repeat:`, one nesting level #advanced_user<br>- delegate a step at one of three levels — inline, assisted or detached — per its `detached:` field, as runner-performed, generic sub-agent, or named agent #user<br>- hand an assisted step's heavy reads to the config-named research agent for a bounded return, resuming that agent by id across a loopback rather than spawning fresh #user<br>- fetch one step's rendered body on demand with `booping render-playbook --step`, so a run loads bodies lazily #user<br>- render and run a playbook against an explicit Project Vault with `--project`, from outside a real project #advanced_user<br>- pause at review gates for the user's explicit verdict, never running past one on silence #user<br>- resume an interrupted run from the state frontier rather than the driving context #user | -| **Running procedures** | Run state machines — per-playbook `states:` machines advanced only by the CLI | - move a run artifact through its named statuses with `booping playbook-transition`, the sole writer of run state #user<br>- guard transitions with `when` conditions and gates declared in `playbook.yaml` #advanced_user<br>- stamp frontmatter on any workdir-relative file from a `frontmatter-update` hook, with `{instance}` interpolation — not only the machine's own artifact #advanced_user<br>- resolve a `script` hook playbook-dir-first then by discovery-root specificity over the shared `_scripts/` roots, passing argv through so one parameterised script replaces near-duplicates #advanced_user<br>- cancel an active run from any non-terminal status — `cancelled` is a real terminal outcome with its own exit hooks #user<br>- address a run artifact directly with `--target {path}`, so a machine can be authored with no declared `artifact:` #advanced_user<br>- report every run artifact's current status with `booping playbook-state` #user | -| **Shaping booping to a project** | Layered configuration — a three-tier deep merge over the core, global and project levels | - override any key at the global level (`~/.config/booping/config.yaml`) or the project level (the Project Vault's `config.yaml`), later levels winning #advanced_user<br>- declare query specs at any config path and run them with `booping query --config` or the `\| query` filter #advanced_user<br>- declare scaffold trees and materialise them with `booping scaffold <dotted.path> <dest>` #advanced_user<br>- declare argv macros under `core.macros` — a `command:` plus a `cwd: repo\|vault` scoping it to the repo or the Project Vault — that rendered bodies call, and stub them for reproducible renders #advanced_user<br>- set the plan-file discovery shape by editing the ordered `core.plans.glob` key — shipped as the single directory entry `plans/*/index.md` #advanced_user<br>- point any assisted step's delegated reads at a different agent with the `core.research_agent` key, beside the cross-review agent key #advanced_user<br>- read any merged value with `booping config-get` and set marker keys with `booping marker-set` #advanced_user | +| **Running procedures** | Run state machines — per-playbook `states:` machines advanced only by the CLI | - move a run artifact through its named statuses with `booping playbook-transition`, the sole writer of run state #user<br>- guard transitions with `when` conditions and gates declared in `playbook.yaml` #advanced_user<br>- stamp frontmatter on any workdir-relative file from a `frontmatter-update` hook, with `{instance}` interpolation — not only the machine's own artifact #advanced_user<br>- append to a list key with `frontmatter-update --append`, creating it when missing and de-duplicating entries, so histories such as `sessions:` need no YAML editing #advanced_user<br>- resolve a `script` hook playbook-dir-first then by discovery-root specificity over the shared `_scripts/` roots, passing argv through so one parameterised script replaces near-duplicates #advanced_user<br>- cancel an active run from any non-terminal status — `cancelled` is a real terminal outcome with its own exit hooks #user<br>- address a run artifact directly with `--target {path}`, so a machine can be authored with no declared `artifact:` #advanced_user<br>- report every run artifact's current status with `booping playbook-state` #user | +| **Shaping booping to a project** | Layered configuration — a three-tier deep merge over the core, global and project levels | - override any key at the global level (`~/.config/booping/config.yaml`) or the project level (the Project Vault's `config.yaml`), later levels winning #advanced_user<br>- declare query specs at any config path and run them with `booping query --config` or the `\| query` filter #advanced_user<br>- declare scaffold trees and materialise them with `booping scaffold <dotted.path> <dest>` — each already-present target skipped and reported on its own, unless `--force` overwrites it #advanced_user<br>- read every `booping scaffold` and `booping frontmatter-update` write back as a unified diff on stdout, silent when a file is unchanged, instead of re-reading the file #advanced_user<br>- declare argv macros under `core.macros` — a `command:` plus a `cwd: repo\|vault` scoping it to the repo or the Project Vault — that rendered bodies call, and stub them for reproducible renders #advanced_user<br>- set the plan-file discovery shape by editing the ordered `core.plans.glob` key — shipped as the single directory entry `plans/*/index.md` #advanced_user<br>- point any assisted step's delegated reads at a different agent with the `core.research_agent` key, beside the cross-review agent key #advanced_user<br>- read any merged value with `booping config-get` and set marker keys with `booping marker-set` #advanced_user | | **Shaping booping to a project** | Targeted lessons — markdown lessons injected into exactly the surfaces they name | - author lessons in either of the two flat `_lessons/` roots — the global level and the Project Vault — each carrying a `targets:` list #advanced_user<br>- target a playbook, a single step, an agent, or a skill (`{playbook}`, `{playbook}/{step}`, `agent:{id}`, `skill:{name}`) #advanced_user<br>- inject matched lessons into a playbook render's `## Lessons` section and into each step's body, alongside internal agent and skill bodies #advanced_user | | **Shaping booping to a project** | Plan and review templates — authored shapes the runs pick from | - author plan templates in the Project Vault's `plan_templates/` for groom to offer at drafting #advanced_user<br>- author review templates in the Project Vault's `review_templates/` or at the global level for code-review runs #advanced_user | | **Shaping booping to a project** | User playbooks — own procedures discovered beside the core ones | - author playbooks at the global level or in the Project Vault's `_playbooks/` — `playbook.md`, an optional `playbook.yaml`, and one directory per step holding its `prompt.md` — names unique across levels #advanced_user<br>- declare a step graph with parallel waves and subgraphs, and opt-in `states:` machines #advanced_user<br>- give a user playbook its own config namespace by copying the core placement rule — a key one playbook owns at `core.{name}_playbook`, a shared key directly under `core` #advanced_user<br>- write playbook and step bodies as Jinja templates over the full context env (`jinja: true`), resolved through a most-specific-wins loader chain #advanced_user<br>- switch a step's prompt variant per model — a step dir holding `prompt.md` plus per-model bodies (`fable-5.md`, `opus-5.md`) #advanced_user<br>- set up a per-step eval suite beside the step — `tests.yaml` plus `promptfooconfig.yaml` and fixtures #advanced_user<br>- scaffold a new playbook with the `playbook-authoring` playbook: brief, decomposition, manifest, then per-step spec, fixtures, prompt, and eval suite driven to green #advanced_user | @@ -29,11 +29,12 @@ shipped only, each line tagged with the roles it serves. Terms follow [glossary. | group | feature | capabilities | | --- | --- | --- | | **Setup** | Project setup — the `setup` playbook that takes a repo from any state to a working booping project | - resolve and write machine-level config with a working `home_dir` #user<br>- scaffold the Project Vault from the config-declared scaffold tree, including `sprints.md` as a live Obsidian Bases view — seeded once, never regenerated #user<br>- write the marker to attach the repo — `vault_path:` for a repo-local Project Vault #user<br>- symlink a repo-local Project Vault into the home dir for Obsidian visibility #user<br>- seed a fresh Project Vault at the highest shipped migration id so no migration is pending #user<br>- detect and skip phases already satisfied, so a re-run reports state instead of changing it #user | -| **Setup** | Project Vault migrations — the `migrate` playbook that brings an existing Project Vault current | - warn from every render surface when the marker's `latest_migration` watermark is behind #user<br>- survey pending plugin-shipped migrations against the marker's watermark #user<br>- apply each pending migration in id order on one up-front approval, one Project Vault commit per migration #user<br>- halt on a failing migration, naming the id, the failure, and a hand-applicable remedy #user | -| **The sprint loop** | Groom — shape a feature, bug, or refactor into a specified, estimated, user-approved plan | - clarify the request at intake before any drafting #user<br>- run its steps inline or assisted rather than one sub-agent per step, apart from the detached research and cross-review passes #user<br>- research the codebase and the web as parallel detached passes #user<br>- draft the plan directory `plans/{slug}/index.md` with milestones, tasks, and SP estimates #user<br>- offer the Project Vault's plan templates as the drafting shape #user<br>- cross-review the draft in a detached pass whenever `core.groom_playbook.cross_review_agent` names a validator agent, skipped when it is null #user<br>- stop at a single approval gate near the end, landing the plan at `ready-for-dev` #user | +| **Setup** | Project Vault migrations — the `migrate` playbook that brings an existing Project Vault current | - warn from every render surface when the marker's `latest_migration` watermark is behind #user<br>- survey pending plugin-shipped migrations against the marker's watermark #user<br>- apply each pending migration in id order on one up-front approval, one Project Vault commit per migration #user<br>- convert an older Project Vault's `_booping/` skill and agent extension files into targeted lessons, removing the converted originals #user<br>- halt on a failing migration, naming the id, the failure, and a hand-applicable remedy #user | +| **The sprint loop** | Groom — shape a feature, bug, or refactor into a specified, estimated, user-approved plan | - clarify the request at intake before any drafting #user<br>- run its steps inline or assisted rather than one sub-agent per step, apart from the detached research and cross-review passes #user<br>- research the codebase and the web as parallel detached passes #user<br>- create the plan directory `plans/{slug}/index.md` with one `booping scaffold` call over the config-declared tree, its `commit:` stamped from repo HEAD at creation #user<br>- draft the plan with milestones, tasks, and SP estimates #user<br>- offer the Project Vault's plan templates as the drafting shape #user<br>- cross-review the draft in a detached pass whenever `core.groom_playbook.cross_review_agent` names a validator agent, skipped when it is null #user<br>- stop at a single approval gate near the end, landing the plan at `ready-for-dev` #user | | **The sprint loop** | Develop — execute a groomed plan one milestone group at a time | - resolve the plan from the invocation, or offer a candidate table of plans ready for development #user<br>- delegate every task to the configured worker agent — the runner never writes application code #user<br>- work one milestone group at a time on the sprint branch, never two workers at once #user<br>- verify the definition of done and the project guardrails, then close the plan at `done` or `fail` — the plan track's terminal statuses #user<br>- hand the finished sprint off to `/playbook retro` #user | | **The sprint loop** | Retro — mine a finished sprint into a project- and plan-specific retrospective | - queue every `done` plan not yet covered by a retrospective (`retro: null`) #user<br>- mine session logs and the sprint diff for what actually happened #user<br>- gather the user's raw feedback and triage issues by root cause #user<br>- save the standalone run artifact `retrospectives/{slug}.md` and stamp each covered plan's `retro:` key, the plan itself staying at `done` #user | | **The sprint loop** | Learn — turn retrospective findings into durable behavior changes | - extract atomic candidates from the retrospective and sweep them against existing lessons #user<br>- show the full playbook table of contents and fetch the targets of each touched playbook #user<br>- confirm a review table with the user before anything is written, each row carrying its exact `targets:` assignment #user<br>- route each accepted item to exactly one destination: a targeted lesson in the Project Vault or the repo's `CLAUDE.md` #user<br>- close the covered retrospective out at `done` #user | +| **The sprint loop** | Session metrics — measured effort per plan, computed from Claude Code session logs | - compute a plan's active work minutes with `booping session-stats`, discounting time spent waiting on a human — question answers and rejected tool calls #user<br>- account each session's tokens — input, output, cache-creation, cache-read — beside the model ids that ran the work #user<br>- stamp the totals as `metrics_*` plan frontmatter automatically on groom and develop transitions #user<br>- surface active minutes and models as `sprints.md` and query columns #user | | **Code review** | Code review — the `code-review` playbook reviewing a confirmed scope end to end | - settle the scope: a delivered plan's diff, the latest commits, or a named ad-hoc target #user<br>- open the run artifact `codereviews/{dir}/{ts}.md`, carrying the reviewed plan or `plan: null` #user<br>- return severity-classified findings from a detached review pass #user<br>- offer the Project Vault's and the global level's review templates as the review shape #user<br>- collect the human verdict on the findings and apply only the approved fixes #user<br>- append the closed review to the plan's `code_reviews:` history, never touching plan status #user | ## NFRs @@ -50,9 +51,7 @@ shipped only, each line tagged with the roles it serves. Terms follow [glossary. - `skills/` and `agents/` committed files — build artefacts of `just build`, never a hand-edited surface - `booping-create-project` — retired; Project Vault creation is the setup playbook over config-declared scaffold trees - the `/chat` and `/help` skills — retired; `/playbook` is the plugin's only shipped skill -- session active-time metrics (`booping session-time`, session stamping) — in flight this cycle, outside the 18 delivered plans - `booping debug-template` — stubbed, not implemented - `playbooks/_fixtures/`, `_partials/`, `_scripts/`, `_lib/` — test fixtures and shared internals beneath the playbook features -- `bin/booping-external-llm-call` and `bin/llm-call-templates/` — internal helper beneath groom's cross-review -- `bin/eval-*.sh` and the report jq filters — eval harness plumbing beneath the eval suites feature +- `scripts/eval-*.sh` and the report jq filters — eval harness plumbing beneath the eval suites feature - CI workflow and repo tooling (`justfile` internals, uv project layout) — machinery, not delivered behavior diff --git a/vault/docs/_specs/index.md b/vault/docs/_specs/index.md index 53f5446a..6c8885ba 100644 --- a/vault/docs/_specs/index.md +++ b/vault/docs/_specs/index.md @@ -18,4 +18,4 @@ Run artifacts live in a per-project **vault** — `~/Claude/{project}/` by defau ## Where it is heading -The project has just converged on its v1.0 shape — everything procedural is a playbook, driven by one skill over a declarative graph-and-states runtime, with the plan, retro and code-review tracks split into independent state machines joined only by plan frontmatter, and the whole surface held by testing infrastructure (committed render snapshots, mdcheck structural gates, promptfoo eval suites). The near-term work is consolidation on top of that foundation: bringing the public documentation current with the playbook framework delivered on PR #19, and incremental runtime instrumentation such as session active-time metrics. +The project has just converged on its v1.0 shape — everything procedural is a playbook, driven by one skill over a declarative graph-and-states runtime, with the plan, retro and code-review tracks split into independent state machines joined only by plan frontmatter, and the whole surface held by testing infrastructure (committed render snapshots, mdcheck structural gates, promptfoo eval suites). The near-term work is consolidation on top of that foundation: bringing the public documentation current with the playbook framework delivered on PR #19. From 6da8ccabbf1010dc0d500c10b6620e7f28faf429 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 14:14:03 +0700 Subject: [PATCH 21/44] docs: 202608101223-docs-refresh @ done --- vault/docs/_runs/202608101223-docs-refresh.md | 42 +++++++++++++++---- vault/docs/_specs/documented.md | 5 +++ vault/docs/_specs/targets.md | 2 +- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/vault/docs/_runs/202608101223-docs-refresh.md b/vault/docs/_runs/202608101223-docs-refresh.md index 8e98b2ff..ede6a3e9 100644 --- a/vault/docs/_runs/202608101223-docs-refresh.md +++ b/vault/docs/_runs/202608101223-docs-refresh.md @@ -1,6 +1,6 @@ --- title: Docs refresh -status: updating +status: done started: 2026-08-10 12:26 commit: 40049f5d68a84ac6dafcf1c0e42171566dbb0098 agents.survey: ac07340b037418656 @@ -10,6 +10,14 @@ changes_reviewed_at: 2026-08-10 12:37 agents.sync-specs: ad066f3e46928c014 agents.targeting-reads: aecc5c3690b9a64d2 targeting_reviewed_at: 2026-08-10 13:43 +agents.changelog: aa77583a7d62949b9 +documented: +- plans/202608072133_learn-targets-consolidation/index.md +- plans/202608081300_session-time-metrics/index.md +- plans/202608081419_justfile-scripts-dead-code-cleanup/index.md +- plans/202608081523_session-metrics-idle-and-tokens/index.md +- plans/202608091310_scaffold-seeded-plan-creation/index.md +completed: 2026-08-10 14:14 --- # Docs refresh @@ -114,14 +122,14 @@ Confirmed 2026-08-10. Spec set current — no spec file refreshed; spec waves sk | Destination | Roles | Changes | Must say | Progress | | --- | --- | --- | --- | --- | -| `README.md` | Users | C3, C4 | C3: the plugin ships one skill, `/playbook`; code review runs via `/playbook code-review` — any standalone `/code-review` skill mention goes. C4: the vault-layout narrative places the CLI log at the vault root (`.booping.log`) and carries no `_booping/` directory. | pending | -| `documentation/index.md` | Users | C19, C3 | C19: the Playbooks nav line loses "*Unstable — work in progress.*". C3: the home page presents `/playbook` as the single shipped skill, code review as one of its playbooks. | pending | -| `documentation/playbook.md` | Advanced users | C19, C1, C7 | C19: the "Unstable — work in progress" warning admonition is removed, no replacement. C1: lesson `targets:` kinds list `skill:{name}` beside `agent:{id}`, injecting into skill bodies. C7: the `frontmatter-update` hook/CLI documents `--append key=val` — list append, create-if-missing, dedup, scalar clash is an error. | pending | -| `documentation/code_review.md` | Users | C3 | The page teaches `/playbook code-review` as the only entry; any framing of a standalone `/code-review` skill is rewritten to the playbook invocation. | pending | -| `documentation/vault.md` | Users, Advanced users | C4, C2, C5, C6, C14 | C4: `.booping.log` sits at the vault root; the layout lists no `_booping/`. C2: project shaping is targeted lessons only — no skill/agent extension files. C5: `/playbook migrate` converts an older vault's extension files into targeted lessons and removes the originals. C6+C14: `sprints.md` carries `active_minutes`, `models` and token columns, fed from `metrics_*` plan frontmatter stamped by the sprint playbooks. | pending | -| `documentation/develop.md` | Users | C6, C14 | Session metrics: active work minutes and models are stamped on groom and develop transitions from Claude Code session logs; `booping session-stats` is the one metrics surface — idle-aware active time (human-wait excluded) plus per-session token accounting. | pending | -| `documentation/groom.md` | Users | C17 | Groom creates the plan directory with one `booping scaffold` call from a config-declared tree; the fresh `index.md` carries a real `commit:` (repo HEAD) from creation. | pending | -| `documentation/project_config.md` | Advanced users | C15, C16, C17 | C15: `booping scaffold` and `booping frontmatter-update` print a unified-diff receipt per write, silent when unchanged. C16: scaffold skips existing target files per-file and reports each, `--force` overwrites — the dest-not-empty error is gone. C17: the plan scaffold tree groom seeds from is a config-declared scaffold tree, named at its dotted path. | pending | +| `README.md` | Users | C3, C4 | C3: the plugin ships one skill, `/playbook`; code review runs via `/playbook code-review` — any standalone `/code-review` skill mention goes. C4: the vault-layout narrative places the CLI log at the vault root (`.booping.log`) and carries no `_booping/` directory. | verified | +| `documentation/index.md` | Users | C19, C3 | C19: the Playbooks nav line loses "*Unstable — work in progress.*". C3: the home page presents `/playbook` as the single shipped skill, code review as one of its playbooks. | verified | +| `documentation/playbook.md` | Advanced users | C19, C1, C7 | C19: the "Unstable — work in progress" warning admonition is removed, no replacement. C1: lesson `targets:` kinds list `skill:{name}` beside `agent:{id}`, injecting into skill bodies. C7: the `frontmatter-update` hook/CLI documents `--append key=val` — list append, create-if-missing, dedup, scalar clash is an error. | verified | +| `documentation/code_review.md` | Users | C3 | The page teaches `/playbook code-review` as the only entry; any framing of a standalone `/code-review` skill is rewritten to the playbook invocation. | verified | +| `documentation/vault.md` | Users, Advanced users | C4, C2, C5, C6, C14 | C4: `.booping.log` sits at the vault root; the layout lists no `_booping/`. C2: project shaping is targeted lessons only — no skill/agent extension files. C5: `/playbook migrate` converts an older vault's extension files into targeted lessons and removes the originals. C6+C14: `sprints.md` carries `active_minutes`, `models` and token columns, fed from `metrics_*` plan frontmatter stamped by the sprint playbooks. | verified | +| `documentation/develop.md` | Users | C6, C14 | Session metrics: active work minutes and models are stamped on groom and develop transitions from Claude Code session logs; `booping session-stats` is the one metrics surface — idle-aware active time (human-wait excluded) plus per-session token accounting. | verified | +| `documentation/groom.md` | Users | C17 | Groom creates the plan directory with one `booping scaffold` call from a config-declared tree; the fresh `index.md` carries a real `commit:` (repo HEAD) from creation. | verified | +| `documentation/project_config.md` | Advanced users | C15, C16, C17 | C15: `booping scaffold` and `booping frontmatter-update` print a unified-diff receipt per write, silent when unchanged. C16: scaffold skips existing target files per-file and reports each, `--force` overwrites — the dest-not-empty error is gone. C17: the plan scaffold tree groom seeds from is a config-declared scaffold tree, named at its dotted path. | verified | ## Not targeted @@ -132,3 +140,19 @@ Confirmed 2026-08-10. Spec set current — no spec file refreshed; spec waves sk - C12 (chore) — quick start and CLAUDE.md already carry no `GEMINI_API_KEY` or external-llm-call references (verified in research). - C13 (chore) — deleted fossils, nothing documented anywhere. - C18 (chore) — the fragment is deleted; the `docs/` surface carries no inventory naming it. + +## Landed + +| Destination | Changes | Outcome | +| --- | --- | --- | +| `README.md` | C3, C4 | verified — 3 fact fixes applied in verify | +| `documentation/index.md` | C19, C3 | verified clean; the runner also dropped the `Playbooks (unstable)` nav label in `mkdocs.yml` (C19) | +| `documentation/playbook.md` | C19, C1, C7 | verified — 2 fact fixes applied | +| `documentation/code_review.md` | C3 | verified — 2 depth fixes applied | +| `documentation/vault.md` | C4, C2, C5, C6, C14 | verified — 3 fact fixes applied | +| `documentation/develop.md` | C6, C14 | verified — 3 fixes applied, plus a runner-applied removal of an unimplemented stop-after-milestone claim | +| `documentation/groom.md` | C17 | verified — 1 fact fix, 1 legacy fix applied | +| `documentation/project_config.md` | C15, C16, C17 | verified — 3 fact fixes applied | +| `CHANGELOG.md` | — | no entry owed — the file already carries v1.0.0 plus a standing `## Unreleased` holding C15–C17; all 19 rows accounted as omitted with reasons | + +5 of 5 in-scope items documented, none withheld; 5 verification findings stay open. diff --git a/vault/docs/_specs/documented.md b/vault/docs/_specs/documented.md index 2b51a9c3..edddfa06 100644 --- a/vault/docs/_specs/documented.md +++ b/vault/docs/_specs/documented.md @@ -57,3 +57,8 @@ One row per work item every one of whose changes is documented; the survey's und | `plans/20260722-jinja-playbooks-composed-step-rendering/index.md` | stale — pre-August, predates the docs process | | `plans/20260722-playbooks-framework-pilot/index.md` | stale — pre-August, predates the docs process | | `plans/20260724-playbook-frontmatter-graph/index.md` | stale — pre-August, predates the docs process | +| `plans/202608072133_learn-targets-consolidation/index.md` | `_runs/202608101223-docs-refresh.md` | +| `plans/202608081300_session-time-metrics/index.md` | `_runs/202608101223-docs-refresh.md` | +| `plans/202608081419_justfile-scripts-dead-code-cleanup/index.md` | `_runs/202608101223-docs-refresh.md` | +| `plans/202608081523_session-metrics-idle-and-tokens/index.md` | `_runs/202608101223-docs-refresh.md` | +| `plans/202608091310_scaffold-seeded-plan-creation/index.md` | `_runs/202608101223-docs-refresh.md` | diff --git a/vault/docs/_specs/targets.md b/vault/docs/_specs/targets.md index fba92bec..021c6684 100644 --- a/vault/docs/_specs/targets.md +++ b/vault/docs/_specs/targets.md @@ -13,7 +13,7 @@ that is rendered, executed, or seeded elsewhere is out of the set. | `documentation/` | MkDocs site source (`docs_dir: documentation`, readthedocs theme, `strict: true`), published to gh-pages on push to `master` via `just docs`. Nav is declared in `mkdocs.yml`: Home, Quick start, Install, Vault, Playbooks, Project config, external agents, then one page per workflow playbook. Pages run 30–480 lines, bare relative `.md` links, sparing `!!!` admonitions, H2-sectioned reference prose with fenced config/command examples. | Users following quick start, per-playbook commands, and status vocabularies; advanced users after the three-tier config merge, templates, lessons, playbook authoring, and external-agent wiring. | The deep public reference — exhaustive on behavior and configuration. Plugin internals (build pipeline, snapshots, evals) stay out; those are contributor territory. | | `docs/` (top-level `*.md` fragments) | Hand-authored plugin-internal fragments, no build step, lazy-loaded at run time via `${CLAUDE_PLUGIN_ROOT}/docs/<name>.md` links from skills, playbook steps, and config. Short (10–65 lines), imperative, one route-specific concern per file (e.g. `how_to_initialize_project.md`, `retro_summary_format.md`). | Agents mid-run following a lazy-linked route; contributors deciding where route-specific detail belongs instead of inlining it. | Exactly what the linking step needs and nothing more — minimum useful context. Anything a human reader would browse belongs on the site instead. | | `CLAUDE.md` | Session-loaded project guide, checked into the repo root. Dense H2 sections (Commands, Layout, Rendering pipelines, Config, Playbooks, Lifecycle, Principles, Editing conventions); one-line-per-item bullets, backticked paths and commands, links into `documentation/` for full references. Schema-over-prose: it points at `src/config.yaml` and `states:` blocks rather than restating them. | Agents (and contributors) opening a session on this repo: where things live, which command gates a change, which files are build artifacts versus live edits. | The map, not the data — orientation and invariants only. Restated config values, status lists, or long procedure prose are drift and out of place. | -| `CHANGELOG.md` | Introduced by this playbook — the repo carries no `CHANGELOG.md` yet, so the changelog step seeds it rather than appends. Repo-root, newest-first releases; entries in the public voice of the release notes (`.claude/skills/release`), user-visible changes only, conventional-commit history as raw material, not content. | Users and advanced users deciding whether to upgrade and what a release changes for their vaults and config. | One entry per release, a few lines each: behavior changes, new config keys, migrations to run. Internal refactors and CI churn stay out. | +| `CHANGELOG.md` | Repo-root, Keep a Changelog shape since 2026-08-09: a standing `## Unreleased` section above dated releases (first entry v1.0.0), past-tense user-voiced bullets grouped Added / Changed / Removed. Entries in the public voice of the release notes (`.claude/skills/release`), user-visible changes only, conventional-commit history as raw material, not content. | Users and advanced users deciding whether to upgrade and what a release changes for their vaults and config. | One entry per release, a few lines each: behavior changes, new config keys, migrations to run. Internal refactors and CI churn stay out. | ## Not surfaces From 4b4eee63958432e329ccbf830dcd9b744e87fd62 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:40:44 +0700 Subject: [PATCH 22/44] feat(booping): scaffold renders tree filename keys Tree keys go through the same Jinja env and --set globals as seed bodies, with the filesystem-safety check re-run on the rendered name so an unsafe render fails before any write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../src/booping/commands/scaffold.py | 13 ++- .../src/booping/context/scaffold.py | 9 +- .../tests/commands/scaffold_test.py | 103 ++++++++++++++++++ booping-python/tests/context/scaffold_test.py | 17 +++ 4 files changed, 138 insertions(+), 4 deletions(-) diff --git a/booping-python/src/booping/commands/scaffold.py b/booping-python/src/booping/commands/scaffold.py index 761b5bc3..241b4150 100644 --- a/booping-python/src/booping/commands/scaffold.py +++ b/booping-python/src/booping/commands/scaffold.py @@ -10,7 +10,7 @@ from booping import logger from booping.context import Context -from booping.context.scaffold import DirNode, FileNode, ScaffoldError, load +from booping.context.scaffold import DirNode, FileNode, Node, ScaffoldError, check_name, load from booping.macros import parse_stub_overrides from booping.rendering import build_source_env from booping.utils import deep_merge, diff_report, parse_set_overrides @@ -86,6 +86,15 @@ def _render_seed(env: Environment, node: FileNode) -> str: raise ScaffoldError(node.path, f"Jinja error in seed content: {exc}") from exc +def _render_name(env: Environment, node: Node) -> str: + try: + name = env.from_string(node.name).render() + except TemplateError as exc: + raise ScaffoldError(node.path, f"Jinja error in filename: {exc}") from exc + check_name(name, node.path) + return name + + def _plan(root: DirNode, dest: Path, env: Environment) -> list[_Write]: """The full tree rendered into memory, parents before children — nothing is written until every node has parsed and rendered.""" @@ -93,7 +102,7 @@ def _plan(root: DirNode, dest: Path, env: Environment) -> list[_Write]: def walk(node: DirNode, base: Path) -> None: for child in node.children: - path = base / child.name + path = base / _render_name(env, child) if isinstance(child, DirNode): writes.append(_Write(path, None)) walk(child, path) diff --git a/booping-python/src/booping/context/scaffold.py b/booping-python/src/booping/context/scaffold.py index dd369680..e216d77d 100644 --- a/booping-python/src/booping/context/scaffold.py +++ b/booping-python/src/booping/context/scaffold.py @@ -115,7 +115,12 @@ def _type_name(value: object) -> str: return type(value).__name__ -def _check_name(name: str, parent_path: str) -> None: +def check_name(name: str, parent_path: str) -> None: + """Reject a filename that would escape the destination directory. + + Public because the command re-runs it on the *rendered* name, which is what + finally hits the filesystem. + """ if "/" in name or name in {".", ".."}: raise ScaffoldError(parent_path, f"unsafe filename key {name!r}") @@ -124,7 +129,7 @@ def _parse_children(raw: dict[str, Any], path: str) -> list[Node]: children: list[Node] = [] for key, value in raw.items(): name = str(key) - _check_name(name, path) + check_name(name, path) children.append(parse_node(value, name, _join(path, name))) return children diff --git a/booping-python/tests/commands/scaffold_test.py b/booping-python/tests/commands/scaffold_test.py index 944cb20e..404cc1a9 100644 --- a/booping-python/tests/commands/scaffold_test.py +++ b/booping-python/tests/commands/scaffold_test.py @@ -323,6 +323,109 @@ def test_receipt_stdout( assert len(lines) == case.line_count +# --- Task 1.1: filename keys render through the seed env -------------------- + + +class KeyCase(NamedTuple): + """One scaffold run whose tree names files by template: tree and `--set` in, + resulting paths and receipt out. + + `files` maps a path relative to the destination to its expected content. + `stderr_contains` empty means the run is expected to succeed. + """ + + tree: str + set_pairs: tuple[str, ...] + files: dict[str, str] + summary: str | None = None + stderr_contains: tuple[str, ...] = () + absent: tuple[str, ...] = () + + +KEY_CASES = [ + pytest.param( + KeyCase( + tree='demo:\n "{{ slug }}.md": "body\\n"\n', + set_pairs=("slug=my-plan",), + files={"my-plan.md": "body\n"}, + summary="scaffolded 2 paths — 1 dirs created, 1 files created, 0 files overwritten", + ), + id="templated-file-key", + ), + pytest.param( + KeyCase( + tree='demo:\n "{{ slug }}":\n "{{ slug }}.md": "in {{ slug }}\\n"\n', + set_pairs=("slug=nested",), + files={"nested/nested.md": "in nested\n"}, + summary="scaffolded 3 paths — 2 dirs created, 1 files created, 0 files overwritten", + ), + id="templated-dir-key-nests", + ), + pytest.param( + KeyCase( + tree='demo:\n "index.md": "body\\n"\n', + set_pairs=("slug=unused",), + files={"index.md": "body\n"}, + summary="scaffolded 2 paths — 1 dirs created, 1 files created, 0 files overwritten", + ), + id="literal-key-untouched", + ), + pytest.param( + KeyCase( + tree='demo:\n "{{ slug }}.md": "body\\n"\n', + set_pairs=("slug=../escape",), + files={}, + stderr_contains=("demo.{{ slug }}.md", "unsafe filename key", "'../escape.md'"), + absent=("../escape.md",), + ), + id="rendered-slash-is-rejected", + ), + pytest.param( + KeyCase( + tree='demo:\n "{{ dots }}": "body\\n"\n', + set_pairs=("dots=..",), + files={}, + stderr_contains=("unsafe filename key", "'..'"), + ), + id="rendered-dotdot-is-rejected", + ), + pytest.param( + KeyCase( + tree='demo:\n "{{ slug }}.md": "body\\n"\n', + set_pairs=(), + files={".md": "body\n"}, + summary="scaffolded 2 paths — 1 dirs created, 1 files created, 0 files overwritten", + ), + id="missing-set-variable-renders-empty", + ), +] + + +@pytest.mark.parametrize("case", KEY_CASES) +def test_filename_keys_render( + case: KeyCase, tmp_path: Path, isolated_xdg_config_home: Path +) -> None: + dest = tmp_path / "out" + args = [arg for pair in case.set_pairs for arg in ("--set", pair)] + result = _scaffold( + tmp_path, isolated_xdg_config_home, "demo", str(dest), *args, tree=case.tree + ) + + if case.stderr_contains: + assert result.returncode == 1 + for fragment in case.stderr_contains: + assert fragment in result.stderr + assert not dest.exists() + else: + assert result.returncode == 0, result.stderr + assert result.stdout.splitlines()[-1] == case.summary + + for rel, content in case.files.items(): + assert (dest / rel).read_text() == content + for rel in case.absent: + assert not (dest / rel).exists() + + def test_unknown_config_path_exits_1( tmp_path: Path, isolated_xdg_config_home: Path ) -> None: diff --git a/booping-python/tests/context/scaffold_test.py b/booping-python/tests/context/scaffold_test.py index 48b802f9..75bf870a 100644 --- a/booping-python/tests/context/scaffold_test.py +++ b/booping-python/tests/context/scaffold_test.py @@ -8,11 +8,28 @@ DirNode, FileNode, ScaffoldError, + check_name, load, parse_node, resolve, ) +# --------------------------------------------------------------------------- +# check_name — the safety gate the command re-runs on rendered names +# --------------------------------------------------------------------------- + +class TestCheckName: + @pytest.mark.parametrize("name", ["a.md", "_references", "...", "a.b.c", "..hidden"]) + def test_accepts_a_plain_filename(self, name: str) -> None: + check_name(name, "t.d") + + @pytest.mark.parametrize("name", ["a/b", "/abs", "trailing/", ".", ".."]) + def test_rejects_an_escaping_name(self, name: str) -> None: + with pytest.raises(ScaffoldError) as exc: + check_name(name, "t.d") + assert exc.value.path == "t.d" + assert repr(name) in str(exc.value) + # --------------------------------------------------------------------------- # parse_node — valid shapes # --------------------------------------------------------------------------- From a95743bf0671033e01571cf46aa1bfdc1e6198da Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:40:53 +0700 Subject: [PATCH 23/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 325 ++++++++++++++++++ .../request.md | 28 ++ 2 files changed, 353 insertions(+) create mode 100644 vault/plans/202608101646_milestone-files-for-dev-agents/index.md create mode 100644 vault/plans/202608101646_milestone-files-for-dev-agents/request.md diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md new file mode 100644 index 00000000..4f927a99 --- /dev/null +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -0,0 +1,325 @@ +--- +title: "Per-milestone plan files handed straight to dev agents" +type: "feature" +status: in-progress +sp: 25 +related_to: null +created: 2026-08-10 16:47 +planned: null +started: 2026-08-10 17:36 +completed: null +code_reviews: [] +sessions: +- 230b182a-514b-434f-926c-e4cb0ddab343 +- e6133dac-19be-4aa4-891a-dd5a6ec4bb64 +retro: null +summary: Plans split into plans/{slug}/milestones/*.md — scaffold-seeded, + state-machine status, develop briefs paths not bodies +commit: 6da8ccabbf1010dc0d500c10b6620e7f28faf429 +reviewed_at: 2026-08-10 17:32 +--- + +# Per-milestone plan files handed straight to dev agents + +## Context + +A groomed plan is one document today: `{vault}/plans/{slug}/index.md` carries context, decisions, architecture and every milestone's goal, task table, per-task DoD and Verify inline. `develop`'s `develop-loop` step reads those milestone sections into runner context and hand-composes a briefing per milestone group — per-milestone request, related files, DoD, Verify, project conventions, scope boundary — which it passes as text to `booping:booping-developer`. The worker never sees the plan; it sees a lossy restatement of it, rebuilt by the runner on every group. + +After this plan, groom writes each milestone as its own file under `plans/{slug}/milestones/`, and develop delegates by handing the worker two paths — the plan's `index.md` for context and scope boundary, the milestone file as the authoritative work contract. Milestone status is persisted run state on the milestone file itself, and `index.md`'s milestone table is regenerated from those files rather than hand-flipped. The observable change: a milestone is a standalone action plan any agent can execute and validate, and the runner spends no context restating it. + +## Decisions + +- **Granularity**: one file per milestone at `plans/{slug}/milestones/{nn}-{kebab}.md` — matches the plan's own structure and the unit develop delegates; grouping stays a develop-time concern that passes 1..N paths in one briefing. +- **Split of content**: `index.md` keeps Context, Decisions, Architecture, the surface-specific sections, Final Verification, Out of scope and CLAUDE.md impact plus a generated milestone table; milestone bodies live only in their own files — one source, no drift. +- **Standard vs template freedom**: only the milestone file's frontmatter contract (`id`, `title`, `sp`, `status`, `plan`) and three required headings (`## Tasks`, `## Definition of Done`, `## Verify`) are fixed; the rest of the body stays shaped by the chosen plan template — this is what keeps programmatic bookkeeping possible without collapsing the five templates into one. +- **Creation**: `booping scaffold` is extended to render tree *keys* through the same Jinja env that already renders file bodies, so a milestone scaffold tree keyed `{{ id }}-{{ slug }}.md` seeds each file; the alternative (a directory per milestone with a literal `milestone.md`) was rejected for path noise, and hand-written files were rejected because the frontmatter seed would be prose-specified and drift. +- **Milestone table**: regenerated from milestone frontmatter by `booping query --glob 'plans/{slug}/milestones/*.md'`, never hand-flipped; the link cell is a plain `[[wikilink]]` — an aliased wikilink needs its pipe escaped inside a table cell in Obsidian and is not worth the fragility. +- **Status**: milestone status is a real state machine — a `states: milestone` entry in `develop/playbook.yaml` with artifact `milestones/{instance}.md`, addressed as `booping playbook-transition develop <to> --state milestone --instance {nn}-{kebab}`. The `{instance}` artifact primitive and `--state`/`--instance` flags already exist; this buys a resumable per-milestone frontier from `booping playbook-state` and gives the table refresh a hook to hang on, instead of prose bookkeeping instructions in the loop body. +- **Briefing**: the worker gets paths and run-time context, never a pasted milestone body; `booping-developer`'s contract gains "read the milestone file at the given path — it is the contract". +- **No back-compat**: develop speaks only the new shape. No fallback branch, no migration of existing plans; plans already at `done` are untouched history. + +## Architecture + +Data flow after the change: + +``` +groom/draft-plan + booping scaffold core.groom_playbook.milestone_scaffold {plan}/milestones \ + --set id=01 --set slug=cli-surface --set title="…" --set sp=3 + → plans/{slug}/milestones/01-cli-surface.md (frontmatter seed + skeleton, body appended by the step) + → index.md ## Milestones table, rendered from the same files by booping query + +develop/provision reads milestone frontmatter (id, title, sp, status) via query → groups +develop/develop-loop briefing = {plan}/index.md + {plan}/milestones/{nn}-*.md paths → booping-developer + on report: DoD checkboxes flipped in the milestone file, + booping playbook-transition develop done --state milestone --instance {nn}-{kebab} + hook: script refresh-milestone-table {plan} → rewrites index.md's table +develop/verify reads every milestone file's DoD + status; index.md table is derived, not authoritative +``` + +The milestone file is the only place a milestone's work is written down. `index.md`'s table and `booping playbook-state`'s per-instance frontier are both projections of milestone frontmatter. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 1 | Scaffold renders tree keys | 3 | done | +| 2 | Milestone file contract in config | 3 | pending | +| 3 | Plan templates carry the milestone file | 3 | pending | +| 4 | Groom writes milestone files | 3 | pending | +| 5 | Milestone state machine and table refresh | 4 | pending | +| 6 | Develop delegates paths, not bodies | 4 | pending | +| 7 | Downstream readers and documentation | 3 | pending | +| 8 | Reports, structure checks and eval fixtures | 2 | pending | + +--- + +### M1: Scaffold renders tree keys — 3 SP | done + +**Goal**: `booping scaffold` renders a tree's filename keys through the same Jinja env it already uses for file bodies, so a tree can name its files from `--set` values. + +**Verify**: `just pytest booping-python/tests/commands/scaffold_test.py booping-python/tests/context/scaffold_test.py` and a manual `booping scaffold` of a templated-key tree into a tmp dir. + +**Tests**: the CLI surface, parametrized — a templated key with `--set` values, a key with no template markers (unchanged), a key rendering to a name containing `/` or `..` (rejected), and the receipt/exit-code shape for each. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Render each node's name through the scaffold render env at walk time, then re-run the filesystem-safety check on the rendered name so a rendered `/` or `..` fails with the existing `ScaffoldError` path (exit 1); keys without template markers stay byte-identical. | `booping-python/src/booping/commands/scaffold.py`, `booping-python/src/booping/context/scaffold.py` | 2 | done | +| 1.2 | Tests for the rendered-key behaviour and its rejection path, matching the existing `RECEIPT_CASES` parametrized style. | `booping-python/tests/commands/scaffold_test.py`, `booping-python/tests/context/scaffold_test.py` | 1 | done | + +#### Task 1.1 DoD + +- [x] A tree key containing `{{ … }}` produces a file named from the rendered value, using the same env and `--set` globals as seed bodies. +- [x] A key rendering to a name with `/`, `.` or `..` exits 1 with the existing unsafe-name error, before any write. +- [x] Literal keys are unaffected — existing scaffold trees produce byte-identical output. +- [x] The receipt still prints per-path lines plus the `scaffolded N paths — …` summary. + +#### Task 1.2 DoD + +- [x] Parametrized cases cover: templated key, literal key, unsafe rendered name, missing `--set` variable. +- [x] Expected filenames and receipt text are written by hand in the test, never derived from the code under test. + +--- + +### M2: Milestone file contract in config — 3 SP | pending + +**Goal**: the milestone file's shape exists as schema — a scaffold tree that seeds it and a shared key describing where milestone files live and which columns project them. + +**Verify**: `booping scaffold core.groom_playbook.milestone_scaffold {tmp-plan}/milestones --set id=01 --set slug=demo --set title="Demo" --set sp=3` produces the seeded file, and `booping query --glob 'plans/{slug}/milestones/*.md' --columns id,title,sp,status --sort id` lists it. + +**Tests**: the config-driven surfaces — scaffolding the milestone tree into a tmp vault yields the exact frontmatter keys and headings, and a query over a two-file milestones dir returns rows ordered by `id`. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Add `core.groom_playbook.milestone_scaffold` — key `{{ id }}-{{ slug }}.md`, seed body carrying frontmatter `id`, `title` (tojson), `sp`, `status: pending`, `plan` and the `# M{{ id }}: {{ title }}` heading plus the three required headings as empty sections; add `milestones: {type: dir}` to `core.groom_playbook.scaffold`. | `src/config.yaml` | 2 | pending | +| 2.2 | Add the shared `core.plans.milestones` key — `glob: milestones/*.md`, `table_columns: [id, title, sp, status]` — read by both groom and develop, and a test that scaffolds the tree and queries the result. | `src/config.yaml`, `booping-python/tests/commands/scaffold_test.py` | 1 | pending | + +#### Task 2.1 DoD + +- [ ] Seeded milestone file frontmatter is exactly `id`, `title`, `sp`, `status`, `plan`, in that order, with `status: pending`. +- [ ] Seeded body carries `# M{id}: {title}` and the empty `## Tasks`, `## Definition of Done`, `## Verify` headings — nothing else. +- [ ] `title` goes through `tojson` like the plan scaffold's, so a colon or quote in a title cannot break the document. +- [ ] A fresh `booping scaffold core.groom_playbook.scaffold` creates `milestones/` alongside `index.md` and `request.md`. + +#### Task 2.2 DoD + +- [ ] `core.plans.milestones` sits directly under `core.plans` (shared by two playbooks, per the config placement rule) and is commented like its neighbours. +- [ ] No column list, glob or directory name is restated in any prompt body — they render from this key. +- [ ] The test asserts hand-written expected frontmatter and query row order. + +--- + +### M3: Plan templates carry the milestone file — 3 SP | pending + +**Goal**: all five plan templates specify a plan as `index.md` plus milestone files, so a drafted plan is written into the new shape by construction. + +**Verify**: read each of the five template files and confirm the milestone-file section and checklist items are present and consistent across them — no repo gate here, and no `mdcheck` run: the structure rules that cover milestone shape are updated in M8. + +**Tests**: none — prose artefacts, covered by `mdcheck` and the report snapshots in M8. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 3.1 | In each template's `# Plan Body`, replace the inline `### M1: …` skeleton under `## Milestones` with the generated milestone table, and add a `## Milestone files` section specifying the per-file body shape for that surface (goal, scope and related files, tasks table, per-task DoD, Verify). | `docs/plan_templates/backend.md`, `docs/plan_templates/frontend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md` | 2 | pending | +| 3.2 | Update each `# Quality Checklist`: milestone-shape items point at the milestone file, add an item that the index table matches the milestone files, keep the `sp` rollup item true against per-file `sp`. | same five files | 1 | pending | + +#### Task 3.1 DoD + +- [ ] Every template names the same required frontmatter keys and the same three required headings; surface-specific guidance lives only in the free part of the body. +- [ ] No template still instructs that milestone bodies live in `index.md`. +- [ ] `documentation.md`'s milestone ordering rubric and each template's surface inserts survive the edit. +- [ ] The per-milestone `Verify` rule (scoped, no whole-repo gates) is stated once per template, in the milestone-file section. + +#### Task 3.2 DoD + +- [ ] Checklist items are verifiable by reading a plan directory, not by intent. +- [ ] `sp` item reads as the sum of milestone-file `sp` values. + +--- + +### M4: Groom writes milestone files — 3 SP | pending + +**Goal**: `draft-plan` seeds and writes one file per milestone and renders `index.md`'s table from them; `present` summarizes from the same source. + +**Verify**: `booping render-playbook groom --step draft-plan` and `--step present` render cleanly and carry no restated column list or directory name. + +**Tests**: none — prompt bodies, covered by the report snapshots in M8. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 4.1 | Rewrite `draft-plan`'s body: after design alignment, the step itself — no sub-step, no delegation — runs one `booping scaffold` call per milestone to seed the file, then writes that milestone's body into the seeded file with a normal file edit, authored by the step against the chosen template's milestone-file section; then it writes `index.md`'s `## Milestones` table from `booping query`. | `playbooks/groom/draft-plan/prompt.md`, `playbooks/groom/draft-plan/opus-5.md`, `playbooks/_partials/plan_templates.md` | 2 | pending | +| 4.2 | Update `present` and `cross-review` for the multi-file plan: the approval screen's milestone table comes from the same query, and the cross-review briefing names `index.md` plus the milestone files as its read set. | `playbooks/groom/present/sonnet-5.md`, `playbooks/groom/cross-review/opus-5.md` | 1 | pending | + +#### Task 4.1 DoD + +- [ ] The step body states the scaffold invocation once, with `--set` values, and never restates the seeded frontmatter keys. +- [ ] The body-writing mechanism is stated explicitly: scaffold seeds the file, the step writes the body into it, one milestone at a time, with no sub-step and no worker agent involved. +- [ ] `plan_templates.md` no longer says the whole body goes under `index.md`. +- [ ] The index table is described as generated output, with the query invocation given verbatim. +- [ ] The plan's `sp` frontmatter is stated as owned by the refresh script (M5.2), not hand-summed by the step. +- [ ] Milestone filenames are specified as `{nn}-{kebab}.md`, zero-padded, ordered by execution order. + +#### Task 4.2 DoD + +- [ ] `present`'s screen reads milestone rows from the query, not from a hand-kept list. +- [ ] The cross-review briefing's read set names both the index and the milestone files, and its return contract is unchanged. + +--- + +### M5: Milestone state machine and table refresh — 4 SP | pending + +**Goal**: milestone status is persisted run state written only by `booping playbook-transition`, and every transition refreshes `index.md`'s milestone table. + +**Verify**: `just pytest booping-python/tests/commands/playbook_transition_test.py booping-python/tests/commands/playbook_state_test.py` plus a manual `booping playbook-transition develop in-progress --state milestone --instance 01-demo --workdir {tmp-plan}` and `booping playbook-state develop --workdir {tmp-plan}` showing the per-instance frontier. + +**Tests**: the hook script's own surface — table replacement in an `index.md` that already has a table, one that has the heading but no table, and idempotency on a second run; plus a transition test driving `--state milestone --instance` through the new states block. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 5.1 | Add the `milestone` states entry to develop — artifact `milestones/{instance}.md`, initial `pending`, transitions `pending → in-progress → done`, `in-progress → blocked`, `blocked → in-progress`, each carrying the table-refresh hook. The `blocked` edge's `when` names the `**Blocked (n/2)**` attempt line develop-loop already writes, relocated into the milestone file's `## Notes`; the two-attempt abort rule stays develop-loop's prose and does not become a gate. | `playbooks/develop/playbook.yaml` | 2 | pending | +| 5.2 | Write `refresh-milestone-table` — a uv-inline Python hook script taking the plan dir, querying `core.plans.milestones`, replacing the `## Milestones` table in `index.md` in place and re-stamping the plan's `sp` frontmatter to the sum of milestone-file `sp` — with tests run by `just pytest` that invoke the script as a subprocess against a tmp plan dir. | `playbooks/develop/_scripts/refresh-milestone-table`, `booping-python/tests/scripts/refresh_milestone_table_test.py` | 2 | pending | + +#### Task 5.1 DoD + +- [ ] `status:` on a milestone file is written by `booping playbook-transition` only — no prompt instructs a hand-edit. +- [ ] Each transition's `when` is stated against observable milestone-file content, and `blocked` records the attempt count the loop already tracks. +- [ ] `booping playbook-state develop --workdir {plan}` reports one row per milestone instance alongside the run machine. +- [ ] The existing `run` machine's statuses and hooks are untouched. + +#### Task 5.2 DoD + +- [ ] The script rewrites only the block between the `## Milestones` heading and the next heading; surrounding prose is byte-identical. +- [ ] The plan's `sp` frontmatter equals the sum of milestone-file `sp` after every run — this is the only writer of `sp` after grooming. +- [ ] Running it twice in a row produces no diff the second time. +- [ ] Columns and glob come from `core.plans.milestones`, never hard-coded. +- [ ] A plan with no `milestones/` directory exits non-zero with a message naming the plan dir, and writes nothing. +- [ ] Tests run under `just pytest` and drive the script as a subprocess against a tmp plan dir. + +--- + +### M6: Develop delegates paths, not bodies — 4 SP | pending + +**Goal**: provision groups from milestone frontmatter, develop-loop briefs the worker with paths, and the worker reads its milestone file itself. + +**Verify**: `booping render-playbook develop` renders cleanly; `bin/booping render src/templates/agents/booping-developer.md.j2` shows the path-based input contract. + +**Tests**: none — prompt and agent bodies, covered by the report snapshots in M8. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 6.1 | Rewrite `develop-loop`'s briefing composition: the briefing carries the plan `index.md` path, the group's milestone file paths, the branch and project conventions, and a bounded return contract — never a pasted milestone body; bookkeeping becomes flipping DoD checkboxes in the milestone file plus the milestone transition. | `playbooks/develop/develop-loop/base.md` | 2 | pending | +| 6.2 | Update `provision` to enumerate and group milestones from `booping query` over the milestone files, and `verify` to check every milestone file's DoD checkboxes and `status: done` rather than reading `index.md`'s body. | `playbooks/develop/provision/base.md`, `playbooks/develop/verify/base.md` | 1 | pending | +| 6.3 | Update the worker agent contract: the briefing names paths, the agent reads the milestone file as its authoritative contract and the plan index for scope boundary, and its report shape stays bounded to what the runner needs. | `src/templates/agents/booping-developer.md.j2` | 1 | pending | + +#### Task 6.1 DoD + +- [ ] The briefing spec lists exactly: plan index path, milestone file paths, branch, conventions, return contract — written out as the literal briefing block the loop composes, so the shape is fixed rather than described. +- [ ] No instruction to inline goal, tasks, DoD or Verify text into the briefing survives. +- [ ] Milestone status flips are stated as the transition invocation with `--state milestone --instance`, and checkbox flips are stated against the milestone file. +- [ ] The one-worker-at-a-time and fresh-agent-per-group rules survive unchanged. + +#### Task 6.2 DoD + +- [ ] Provision's grouping table is fed by the query, and `core.sprint.max_milestones_per_agent` still bounds a group. +- [ ] Verify reads the milestone files and treats `index.md`'s table as derived. + +#### Task 6.3 DoD + +- [ ] The rendered agent body tells the worker which path is the contract and which is context, against the same briefing block M6.1 fixes — no new invocation flag or YAML key is introduced, only the briefing's `## Inputs` lines change. +- [ ] The worker is still barred from vault writes — checkbox and status writes stay the runner's. +- [ ] The report format stays a bounded per-milestone block. + +--- + +### M7: Downstream readers and documentation — 3 SP | pending + +**Goal**: every surface that reads a plan "in full" follows the milestone files, and the hand-authored docs describe the new shape. + +**Verify**: `booping render-playbook retro --step prepare` and `booping render-playbook code-review --step review` render cleanly; `just docs`. + +**Tests**: none — prompt and documentation surfaces. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 7.1 | Extend the plan read set in retro and code-review: `prepare`'s full-plan read, the lesson-check partial's "frontmatter, milestones, tasks, DoDs, Verify" instruction, and the review pass's DoD-checkbox cross-reference all name `index.md` plus `milestones/*.md`. | `playbooks/retro/prepare/base.md`, `src/templates/_partials/_plan_lesson_check.j2`, `playbooks/code-review/review/opus-5.md` | 1 | pending | +| 7.2 | Update the public docs and the repo guide: plan shape in the vault doc, develop's resume description, groom's plan description, README's plan-track narrative, and the CLAUDE.md Lifecycle bullet that defines a plan directory. | `documentation/vault.md`, `documentation/develop.md`, `documentation/groom.md`, `README.md`, `CLAUDE.md` | 2 | pending | + +#### Task 7.1 DoD + +- [ ] No surface still assumes milestone bodies live in `index.md`. +- [ ] Each read set is stated once, as a path pair, with no restated milestone anatomy. + +#### Task 7.2 DoD + +- [ ] `documentation/vault.md` describes the plan directory including `milestones/`. +- [ ] `documentation/develop.md`'s resume prose points at milestone status, not checkbox scanning in the index. +- [ ] CLAUDE.md's Lifecycle and Config sections name the milestone file contract and `core.plans.milestones`. +- [ ] No stale reference to the single-file plan survives in `README.md`. + +--- + +### M8: Reports, structure checks and eval fixtures — 2 SP | pending + +**Goal**: the committed playbook reports, structure rules and eval fixtures match the new plan shape. + +**Verify**: `just snapshots` (diff read and reported, not accepted), `just mdcheck`, `just lint`, `just typecheck`. + +**Tests**: the eval fixtures themselves — a groom fixture plan and a develop fixture plan in the new shape, so suites exercise milestone files rather than a single document. + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 8.1 | Update the hermetic render fixture and eval fixtures to the multi-file plan shape, and adjust any `mdcheck` rule that asserts milestone structure inside `index.md`. | `playbooks/_fixtures/vault/`, `playbooks/groom/*/_fixtures/`, `playbooks/develop/*/_fixtures/`, `scripts/mdcheck.py` | 1 | pending | +| 8.2 | Run `just snapshots`, read the diff for `groom` and `develop`, and report it for the user to accept; run `just ci` minus the snapshot-accept step and fix what it surfaces. | `playbooks/groom/_reports/output.md`, `playbooks/develop/_reports/output.md` (read-only) | 1 | pending | + +#### Task 8.1 DoD + +- [ ] Fixture plans carry `milestones/` with at least two milestone files and a generated index table. +- [ ] `just mdcheck` passes against the rendered reports. + +#### Task 8.2 DoD + +- [ ] The snapshot diff is reported to the user; `just snapshots-accept` is **not** run by the worker or the runner. +- [ ] `just lint`, `just typecheck` and `just pytest` are green. + +--- + +## Final Verification + +- [ ] `just ci` green (`lint typecheck pytest snapshots mdcheck`), with the snapshot step's diff reported for user acceptance rather than accepted automatically. +- [ ] `just build` renders cleanly and `git diff -- skills/ agents/` shows only the intended agent-contract change. +- [ ] An end-to-end dry run: groom a throwaway plan in the fixture vault, confirm `milestones/*.md` exist with seeded frontmatter and a generated index table, drive one milestone transition and confirm the table refreshes. +- [ ] `booping playbook-state develop --workdir {plan}` reports the per-milestone frontier alongside the run machine. +- [ ] No rendered body restates the milestone frontmatter keys, glob or table columns — they come from `core.plans.milestones` and the scaffold tree. + +## Out of scope + +- No migration of existing plans and no back-compat branch in develop — plans groomed before this change are history. +- No change to the `run` state machine of groom or develop beyond the added `milestone` states entry. +- No per-milestone git branching or parallel workers — one worker at a time on the sprint branch stays the rule. +- No `query` feature work beyond what exists (no link column, no per-plan query spec); the index table's link cell is a plain wikilink written by the refresh script. +- No changes to retro, learn or code-review state machines — only their plan read sets. + +## CLAUDE.md impact + +- **Lifecycle** — the plan-track bullet gains the milestone-file shape (`plans/{slug}/milestones/{nn}-{kebab}.md`) and the note that milestone status is run state. +- **Config** — placement note gains `core.plans.milestones`; the scaffold-tree bullet gains rendered filename keys. +- **Playbooks** — the run-state bullet gains the `milestone` machine as an example of `{instance}` addressing. diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/request.md b/vault/plans/202608101646_milestone-files-for-dev-agents/request.md new file mode 100644 index 00000000..9b687057 --- /dev/null +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/request.md @@ -0,0 +1,28 @@ +## Request + +> i'd like to split groomed plans into separate milestones files to handle it later by a dev-agents without pipelining them through the runner to agents but just to send the file to development agents + +## Task type + +`feature` — a new user-visible capability: the groomed plan becomes a multi-file artifact and develop hands a milestone file path to the worker instead of a composed briefing. + +- Not `bug` — nothing diverges from expected behaviour; today's single-file plan works as specified. +- Not `refactoring` — the vault artifact shape changes (new files users read in Obsidian) and develop's delegation contract changes, so behaviour is user-visible on both surfaces. + +## Problem + +Today `groom` writes one plan document, `{vault}/plans/{slug}/index.md`, holding every milestone's tasks, DoD and Verify inline. `develop`'s `develop-loop` step then reads that document into runner context and **composes** a briefing per milestone group — per-milestone request, related files, DoD and Verify, project conventions, scope boundary — and passes that composed text to `booping:booping-developer`. + +That pipelining costs runner context on every group and makes the briefing a lossy re-statement of a document that already exists. The plan should instead be split into per-milestone files at groom time, so develop delegates by handing the worker a **path** plus minimal run-time context, and the milestone file itself is the contract. + +## Clarifications and Decisions + +- Granularity: one file per milestone, at `plans/{slug}/milestones/{nn}-{kebab}.md`. +- `index.md` keeps overview, decisions, architecture, risks and a milestone table with links; milestone bodies live only in their own files — single source, no drift. +- Bookkeeping (DoD checkboxes, task rows, milestone status) lands in the milestone file; each milestone file is a standalone action plan any agent can execute and validate. +- No back-compat: new plans only, no fallback path in develop, no migration of existing plans. +- Bookkeeping is programmatic, not a separate agent step: milestone status in frontmatter flipped by `booping frontmatter-update`; DoD checkboxes flipped by the runner in the body. +- The plan-template tension is resolved by standardising only the milestone file's frontmatter contract plus two required headings (`## Definition of Done`, `## Verify`); body prose stays template-shaped. +- The dev-agent briefing carries two paths — `index.md` for context and scope boundary, the milestone file as the authoritative work contract — and no pasted bodies. +- Milestone files are created by `booping scaffold` (frontmatter seed) with the body appended; the index milestone table is rendered from a `booping query --glob 'plans/{slug}/milestones/*.md'` over their frontmatter. +- No post-implementation prose-shape reshape milestone is expected. From 057c2ab2f0d3f67c8b7030e9d24c4bc2f11efa92 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:46:16 +0700 Subject: [PATCH 24/44] feat(booping): milestone file contract in config core.groom_playbook.milestone_scaffold seeds one file per milestone with a fixed identity frontmatter and the three required headings; the shared core.plans.milestones key describes where they live and how index.md's table projects them. Ids are quoted so zero-padded values do not read back as octal ints and scramble sorting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../tests/commands/scaffold_test.py | 80 ++++++++++++++++++- src/config.yaml | 35 ++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/booping-python/tests/commands/scaffold_test.py b/booping-python/tests/commands/scaffold_test.py index 404cc1a9..a0caefa2 100644 --- a/booping-python/tests/commands/scaffold_test.py +++ b/booping-python/tests/commands/scaffold_test.py @@ -1,10 +1,12 @@ from __future__ import annotations +import json import subprocess from pathlib import Path from typing import NamedTuple import pytest +import yaml PLUGIN_ROOT = Path(__file__).resolve().parents[3] BOOPING_BIN = PLUGIN_ROOT / "bin" / "booping" @@ -514,8 +516,6 @@ def test_core_playbook_scaffold_tree(tmp_path: Path) -> None: def test_core_vault_scaffold_seeds_sprints_base_fence(tmp_path: Path) -> None: - import yaml - dest = tmp_path / "vault" result = _run("scaffold", "core.setup_playbook.scaffold", str(dest), cwd=tmp_path) assert result.returncode == 0, result.stderr @@ -584,6 +584,82 @@ def test_core_vault_scaffold_non_empty_destination_fills_the_gaps( assert (dest / "stray.md").read_text() == "x\n" +def test_core_milestone_scaffold_seeds_the_milestone_contract(tmp_path: Path) -> None: + plan = tmp_path / "vault" / "plans" / "demo" + result = _run( + "scaffold", + "core.groom_playbook.milestone_scaffold", + str(plan / "milestones"), + "--set", + "id=01", + "--set", + "slug=cli-surface", + "--set", + "title=Demo: it's \"fine\"", + "--set", + "sp=3", + "--set", + "plan=plans/demo/index.md", + cwd=tmp_path, + ) + assert result.returncode == 0, result.stderr + + text = (plan / "milestones" / "01-cli-surface.md").read_text() + front, body = text.split("---\n", 2)[1:] + assert list(yaml.safe_load(front).items()) == [ + ("id", "01"), + ("title", "Demo: it's \"fine\""), + ("sp", 3), + ("status", "pending"), + ("plan", "plans/demo/index.md"), + ] + assert body == ( + '\n# M01: Demo: it\'s "fine"\n' + "\n## Tasks\n" + "\n## Definition of Done\n" + "\n## Verify\n" + ) + + +def test_scaffolded_milestones_query_by_the_shared_key(tmp_path: Path) -> None: + vault = tmp_path / "vault" + plan = vault / "plans" / "demo" + result = _run( + "scaffold", "core.groom_playbook.scaffold", str(plan), + "--set", "title=Demo", "--set", "type=feature", + "--stub-macro", "core.macros.date=20260810-00-00", + "--stub-macro", "core.macros.git_commit=abc1234", + cwd=tmp_path, + ) + assert result.returncode == 0, result.stderr + assert (plan / "milestones").is_dir() + + # 01 vs 08: unquoted zero-padded ids would land as int and str in one column. + for id_, slug, sp in (("10", "third", "1"), ("01", "first", "3"), ("08", "second", "2")): + result = _run( + "scaffold", "core.groom_playbook.milestone_scaffold", str(plan / "milestones"), + "--set", f"id={id_}", "--set", f"slug={slug}", "--set", f"title=M {id_}", + "--set", f"sp={sp}", "--set", "plan=plans/demo/index.md", cwd=tmp_path, + ) + assert result.returncode == 0, result.stderr + + result = _run( + "query", "--project", str(vault), "--glob", "plans/demo/milestones/*.md", + "--columns", "id,title,sp,status", "--sort", "id", "--output", "json", + cwd=tmp_path, + ) + assert result.returncode == 0, result.stderr + rows = [ + {k: v for k, v in row.items() if k not in ("path", "slug")} + for row in json.loads(result.stdout) + ] + assert rows == [ + {"id": "01", "title": "M 01", "sp": 3, "status": "pending"}, + {"id": "08", "title": "M 08", "sp": 2, "status": "pending"}, + {"id": "10", "title": "M 10", "sp": 1, "status": "pending"}, + ] + + def test_logs_one_scaffold_line_when_project_attached( tmp_path: Path, isolated_xdg_config_home: Path ) -> None: diff --git a/src/config.yaml b/src/config.yaml index 977c3585..3e2e4b8a 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -62,6 +62,14 @@ core: # and the engine still honours list order for a vault declaring several. glob: - plans/*/index.md + # A milestone is a file inside its plan directory, seeded by + # `core.groom_playbook.milestone_scaffold`. Shared by groom (writes them, + # renders index.md's table) and develop (groups them, delegates their paths): + # `glob` is relative to the plan directory, `table_columns` is the projection + # index.md's `## Milestones` table is rendered with. + milestones: + glob: milestones/*.md + table_columns: [id, title, sp, status] sprint: default_threshold_sp: 35 @@ -116,6 +124,33 @@ core: # {{ title }} request.md: "" + milestones: + type: dir + # Scaffold tree materialised once per milestone by + # `booping scaffold core.groom_playbook.milestone_scaffold {vault}/plans/<slug>/milestones + # --set id=<nn> --set slug=<kebab> --set title=<title> --set sp=<sp> --set plan=<plan-path>`. + # Sole definition of the milestone file's identity frontmatter and required + # headings; everything below them is the plan template's business. + # `id` is quoted like the rest: bare zero-padded ids are read back as octal + # ints (`01` -> 1) except where a digit forbids it (`08` stays a string), + # which mixes types in one column and scrambles `--sort id`. + milestone_scaffold: + "{{ id }}-{{ slug }}.md": | + --- + id: {{ id | tojson }} + title: {{ title | tojson }} + sp: {{ sp }} + status: pending + plan: {{ (plan | default('')) | tojson }} + --- + + # M{{ id }}: {{ title }} + + ## Tasks + + ## Definition of Done + + ## Verify agents: booping-researcher: internal: true From f23725b2ae4adeb9870a838302215aaefdaaa33a Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:46:16 +0700 Subject: [PATCH 25/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index 4f927a99..d205c7d0 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -64,7 +64,7 @@ The milestone file is the only place a milestone's work is written down. `index. | id | title | sp | status | | --- | --- | --- | --- | | 1 | Scaffold renders tree keys | 3 | done | -| 2 | Milestone file contract in config | 3 | pending | +| 2 | Milestone file contract in config | 3 | done | | 3 | Plan templates carry the milestone file | 3 | pending | | 4 | Groom writes milestone files | 3 | pending | | 5 | Milestone state machine and table refresh | 4 | pending | @@ -101,7 +101,7 @@ The milestone file is the only place a milestone's work is written down. `index. --- -### M2: Milestone file contract in config — 3 SP | pending +### M2: Milestone file contract in config — 3 SP | done **Goal**: the milestone file's shape exists as schema — a scaffold tree that seeds it and a shared key describing where milestone files live and which columns project them. @@ -111,21 +111,21 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 2.1 | Add `core.groom_playbook.milestone_scaffold` — key `{{ id }}-{{ slug }}.md`, seed body carrying frontmatter `id`, `title` (tojson), `sp`, `status: pending`, `plan` and the `# M{{ id }}: {{ title }}` heading plus the three required headings as empty sections; add `milestones: {type: dir}` to `core.groom_playbook.scaffold`. | `src/config.yaml` | 2 | pending | -| 2.2 | Add the shared `core.plans.milestones` key — `glob: milestones/*.md`, `table_columns: [id, title, sp, status]` — read by both groom and develop, and a test that scaffolds the tree and queries the result. | `src/config.yaml`, `booping-python/tests/commands/scaffold_test.py` | 1 | pending | +| 2.1 | Add `core.groom_playbook.milestone_scaffold` — key `{{ id }}-{{ slug }}.md`, seed body carrying frontmatter `id`, `title` (tojson), `sp`, `status: pending`, `plan` and the `# M{{ id }}: {{ title }}` heading plus the three required headings as empty sections; add `milestones: {type: dir}` to `core.groom_playbook.scaffold`. | `src/config.yaml` | 2 | done | +| 2.2 | Add the shared `core.plans.milestones` key — `glob: milestones/*.md`, `table_columns: [id, title, sp, status]` — read by both groom and develop, and a test that scaffolds the tree and queries the result. | `src/config.yaml`, `booping-python/tests/commands/scaffold_test.py` | 1 | done | #### Task 2.1 DoD -- [ ] Seeded milestone file frontmatter is exactly `id`, `title`, `sp`, `status`, `plan`, in that order, with `status: pending`. -- [ ] Seeded body carries `# M{id}: {title}` and the empty `## Tasks`, `## Definition of Done`, `## Verify` headings — nothing else. -- [ ] `title` goes through `tojson` like the plan scaffold's, so a colon or quote in a title cannot break the document. -- [ ] A fresh `booping scaffold core.groom_playbook.scaffold` creates `milestones/` alongside `index.md` and `request.md`. +- [x] Seeded milestone file frontmatter is exactly `id`, `title`, `sp`, `status`, `plan`, in that order, with `status: pending`. +- [x] Seeded body carries `# M{id}: {title}` and the empty `## Tasks`, `## Definition of Done`, `## Verify` headings — nothing else. +- [x] `title` goes through `tojson` like the plan scaffold's, so a colon or quote in a title cannot break the document. +- [x] A fresh `booping scaffold core.groom_playbook.scaffold` creates `milestones/` alongside `index.md` and `request.md`. #### Task 2.2 DoD -- [ ] `core.plans.milestones` sits directly under `core.plans` (shared by two playbooks, per the config placement rule) and is commented like its neighbours. -- [ ] No column list, glob or directory name is restated in any prompt body — they render from this key. -- [ ] The test asserts hand-written expected frontmatter and query row order. +- [x] `core.plans.milestones` sits directly under `core.plans` (shared by two playbooks, per the config placement rule) and is commented like its neighbours. +- [x] No column list, glob or directory name is restated in any prompt body — they render from this key. +- [x] The test asserts hand-written expected frontmatter and query row order. --- From ae2707d5c014a350b31d66735ef557fc7e31ea9a Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:50:34 +0700 Subject: [PATCH 26/44] feat(groom): plan templates specify index.md plus milestone files Each template's Milestones section is now a generated table and a new Milestone files section giving the per-file body shape for that surface; checklists check the plan directory instead of a single file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- docs/plan_templates/backend.md | 49 +++++++++++++--------------- docs/plan_templates/claude_skill.md | 31 +++++++++--------- docs/plan_templates/cli.md | 31 +++++++++--------- docs/plan_templates/documentation.md | 32 +++++++++--------- docs/plan_templates/frontend.md | 31 +++++++++--------- 5 files changed, 87 insertions(+), 87 deletions(-) diff --git a/docs/plan_templates/backend.md b/docs/plan_templates/backend.md index 99ac0f4a..c96e54bf 100644 --- a/docs/plan_templates/backend.md +++ b/docs/plan_templates/backend.md @@ -24,35 +24,30 @@ How this change fits the existing system. Integration points. Reference concrete ## Milestones -### M1: {Milestone name} — {SP} SP | pending +Generated table — one row per milestone file, projected with `core.plans.milestones.table_columns` by `booping query`. Derived output: no hand-written rows, no milestone bodies in this file. -**Goal**: one sentence — what changes after this milestone. +## Milestone files -**Verify**: exact commands (or observable outcomes) to confirm this milestone is done, scoped to what this milestone changed — a targeted test path, one invocation, a diff. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. +One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: -| Task | Description | Files | SP | Status | -|------|-------------|-------|----|--------| -| 1.1 | ... | `path/to/file.ext` | 2 | pending | -| 1.2 | ... | `path/to/other.ext` | 1 | pending | +- **Goal** — one sentence directly under the H1: what changes in the system after this milestone. +- **Scope** — the modules, endpoints or tables in play and the files this milestone touches, so it executes without reading another milestone. +- `## Tasks` — one row per task: -#### Task 1.1 DoD + | Task | Description | Files | SP | Status | + |------|-------------|-------|----|--------| + | 1.1 | ... | `path/to/file.ext` | 2 | pending | + | 1.2 | ... | `path/to/other.ext` | 1 | pending | -- [ ] Specific, verifiable criterion. -- [ ] Test / verification command passes. +- `## Definition of Done` — one `### Task {n}.{m}` block per task, checkbox bullets only: a specific verifiable criterion, the test / verification command that proves it. A code sketch belongs here when the shape is non-obvious — interface only, `...` in method bodies: -#### Task 1.1 Code sketch *(only when the shape is non-obvious)* + ``` + class NewThing: + def method(self): + ... + ``` -``` -class NewThing: - def method(self): - ... # interface only — implementers flesh out -``` - ---- - -### M2: ... - ---- +- `## Verify` — exact commands (or observable outcomes) confirming this milestone: a targeted test path, one invocation, a diff. Scoped to what this milestone changed — whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in `index.md`'s Final Verification, never per milestone. ## Implementation Order *(when milestones have dependencies)* @@ -119,12 +114,12 @@ Either name specific sections to update with an owning task, or state "No CLAUDE # Quality Checklist -Verify before leaving `in-spec`. Every item must be satisfiable by reading the plan file alone. +Verify before leaving `in-spec`. Every item must be satisfiable by reading the plan directory alone. ## Frontmatter - [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. -- [ ] `sp` equals the sum of per-task SP across milestones. +- [ ] `sp` equals the sum of the milestone files' `sp`. - [ ] `summary` is set (non-empty, single line, ≤ ~120 chars) for `feature` and `refactoring` plans. ## Content @@ -133,7 +128,9 @@ Verify before leaving `in-spec`. Every item must be satisfiable by reading the p - [ ] For features and refactorings: `summary` is phrased as the user/internal-visible outcome, not engineering output. - [ ] Definition of Done bullets are testable (verifiable by command or inspectable output). - [ ] Decisions table lists real alternatives — no empty "Alternative considered" rows. -- [ ] Every milestone has a `Verify` command or verifiable outcome, scoped to what that milestone changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. +- [ ] Every milestone is a file in `milestones/` carrying its own goal, tasks, DoD and Verify — no milestone body in `index.md`. +- [ ] `index.md`'s milestone table has one row per milestone file and matches their frontmatter. +- [ ] Every milestone file's `## Verify` is a command or verifiable outcome scoped to what that milestone changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to `index.md`'s Final Verification. - [ ] Every task lists exact file paths, not "related files" or "somewhere in X". - [ ] Every task DoD uses checkboxes, not prose. - [ ] Code sketches use `...` in method bodies — agents implement from interfaces, not by copying literal code. @@ -145,7 +142,7 @@ Verify before leaving `in-spec`. Every item must be satisfiable by reading the p - [ ] No "handle edge cases", "add error handling", "clean up" as standalone tasks. - [ ] No "either X or Y" unresolved — pick one, justify in Decisions. - [ ] No task spanning unrelated concerns (model + API + frontend in one row). -- [ ] No milestone that requires reading more than the plan file to execute. +- [ ] No milestone that requires reading more than its own file and `index.md` to execute. ## External references validated diff --git a/docs/plan_templates/claude_skill.md b/docs/plan_templates/claude_skill.md index 19707b1e..100ad782 100644 --- a/docs/plan_templates/claude_skill.md +++ b/docs/plan_templates/claude_skill.md @@ -21,23 +21,22 @@ How the skill interacts with other skills via shared config (statuses, agents, t ## Milestones -### M1: {Milestone name} — {SP} SP | pending +Generated table — one row per milestone file, projected with `core.plans.milestones.table_columns` by `booping query`. Derived output: no hand-written rows, no milestone bodies in this file. -**Goal**: one sentence — the observable change in the rendered skill or shared config. +## Milestone files -**Verify**: render and sanity check (e.g. `bin/booping render src/templates/skills/<name>.md.j2` and review output) — the rebuild plus the surfaces this milestone touched. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. +One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: -| Task | Description | Files | SP | Status | -|------|-------------|-------|----|--------| -| 1.1 | ... | `src/templates/skills/<name>.md.j2`, `src/config.yaml` | 2 | pending | +- **Goal** — one sentence directly under the H1: the observable change in the rendered skill or shared config. +- **Scope** — the templates, partials, config keys and rendered artefacts this milestone touches, and which other skills read the same config. +- `## Tasks` — one row per task: -#### Task 1.1 DoD + | Task | Description | Files | SP | Status | + |------|-------------|-------|----|--------| + | 1.1 | ... | `src/templates/skills/{name}.md.j2`, `src/config.yaml` | 2 | pending | -- [ ] Rendered skill diff matches intended shape. -- [ ] No hardcoded values that duplicate config. -- [ ] Lazy-load links resolve. - ---- +- `## Definition of Done` — one `### Task {n}.{m}` block per task, checkbox bullets only: rendered diff matches the intended shape, no hardcoded values that duplicate config, lazy-load links resolve. +- `## Verify` — render and sanity check (e.g. `bin/booping render src/templates/skills/{name}.md.j2` and review output) — the rebuild plus the surfaces this milestone touched. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in `index.md`'s Final Verification, never per milestone. ## Final Verification @@ -60,7 +59,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat ## Frontmatter - [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. -- [ ] `sp` equals the sum of per-task SP across milestones. +- [ ] `sp` equals the sum of the milestone files' `sp`. ## Content @@ -68,8 +67,10 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat - [ ] DoD bullets are verifiable by reading the rendered output or a diff. - [ ] Every task lists exact template / partial / config paths. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` step that includes a rebuild and stays scoped to the surfaces it touched — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. -- [ ] Each milestone executable from a fresh session with only the plan as context. +- [ ] Every milestone is a file in `milestones/` carrying its own goal, tasks, DoD and Verify — no milestone body in `index.md`. +- [ ] `index.md`'s milestone table has one row per milestone file and matches their frontmatter. +- [ ] Every milestone file's `## Verify` includes a rebuild and stays scoped to the surfaces that milestone touched — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to `index.md`'s Final Verification. +- [ ] Each milestone file executable from a fresh session with only it and `index.md` as context. ## Skill-design hygiene diff --git a/docs/plan_templates/cli.md b/docs/plan_templates/cli.md index c619b404..b2efacaf 100644 --- a/docs/plan_templates/cli.md +++ b/docs/plan_templates/cli.md @@ -21,23 +21,22 @@ How the CLI fits the surrounding toolchain. Input sources, output sinks, side ef ## Milestones -### M1: {Milestone name} — {SP} SP | pending +Generated table — one row per milestone file, projected with `core.plans.milestones.table_columns` by `booping query`. Derived output: no hand-written rows, no milestone bodies in this file. -**Goal**: one sentence — the observable change in CLI behavior. +## Milestone files -**Verify**: exact invocation + expected output (e.g. `./bin/mytool --flag arg 2>&1 | diff - tests/fixtures/expected.txt`). Scoped to this milestone — whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. +One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: -| Task | Description | Files | SP | Status | -|------|-------------|-------|----|--------| -| 1.1 | ... | `bin/mytool`, `tests/fixtures/*.txt` | 2 | pending | +- **Goal** — one sentence directly under the H1: the observable change in CLI behavior. +- **Scope** — the subcommands and flags in play, the files this milestone touches, and any caller (skill, script) whose expected output shape it affects. +- `## Tasks` — one row per task: -#### Task 1.1 DoD + | Task | Description | Files | SP | Status | + |------|-------------|-------|----|--------| + | 1.1 | ... | `bin/mytool`, `tests/fixtures/*.txt` | 2 | pending | -- [ ] Happy-path invocation produces expected output. -- [ ] `--help` reflects the new surface. -- [ ] Exit code matches the contract (0 on success, ≠0 on defined failure modes). - ---- +- `## Definition of Done` — one `### Task {n}.{m}` block per task, checkbox bullets only: happy-path invocation produces the expected output, `--help` reflects the new surface, exit code matches the contract (0 on success, ≠0 on defined failure modes). +- `## Verify` — exact invocation + expected output (e.g. `./bin/mytool --flag arg 2>&1 | diff - tests/fixtures/expected.txt`). Scoped to this milestone — whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in `index.md`'s Final Verification, never per milestone. ## I/O contract @@ -71,7 +70,7 @@ Name sections to update (new CLI in the `CLI` section, new inlining point in a s ## Frontmatter - [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. -- [ ] `sp` equals the sum of per-task SP across milestones. +- [ ] `sp` equals the sum of the milestone files' `sp`. ## Content @@ -79,8 +78,10 @@ Name sections to update (new CLI in the `CLI` section, new inlining point in a s - [ ] DoD bullets are verifiable by invocation + output diff. - [ ] Every task lists exact files. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` invocation scoped to what it changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. -- [ ] Each milestone executable from a fresh session with only the plan as context. +- [ ] Every milestone is a file in `milestones/` carrying its own goal, tasks, DoD and Verify — no milestone body in `index.md`. +- [ ] `index.md`'s milestone table has one row per milestone file and matches their frontmatter. +- [ ] Every milestone file's `## Verify` is an invocation scoped to what that milestone changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to `index.md`'s Final Verification. +- [ ] Each milestone file executable from a fresh session with only it and `index.md` as context. ## I/O contract diff --git a/docs/plan_templates/documentation.md b/docs/plan_templates/documentation.md index 013c6e90..c96d326f 100644 --- a/docs/plan_templates/documentation.md +++ b/docs/plan_templates/documentation.md @@ -35,24 +35,22 @@ Order pages so each milestone produces something reviewable in isolation. A typi 4. **Cross-references and stale-reference cleanup** — README link, skill lazy-load wiring, project-conventions doc updates. 5. **Reshape** — pause for user IA review against the rendered site; apply prose/structure changes uncovered by reading the built output. -### M1: {Milestone name} — {SP} SP | pending +The table below is generated — one row per milestone file, projected with `core.plans.milestones.table_columns` by `booping query`. Derived output: no hand-written rows, no milestone bodies in this file. -**Goal**: one sentence — what page(s) or pipeline component lands. +## Milestone files -**Verify**: build/serve the site locally and load the pages this milestone changed; check their cross-links resolve. Whole-repo gates — a strict full-site build, a pushed branch confirming the CI workflow green, an aggregate `ci` target — run once in Final Verification, never per milestone. +One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: -| Task | Description | Files | SP | Status | -|------|-------------|-------|----|--------| -| 1.1 | ... | `documentation/<page>.md`, `mkdocs.yml` | 2 | pending | +- **Goal** — one sentence directly under the H1: what page(s) or pipeline component lands. +- **Scope** — the pages this milestone writes or touches, their place in the page tree, and the surfaces that link to them. +- `## Tasks` — one row per task: -#### Task 1.1 DoD + | Task | Description | Files | SP | Status | + |------|-------------|-------|----|--------| + | 1.1 | ... | `documentation/{page}.md`, `mkdocs.yml` | 2 | pending | -- [ ] Page renders in the local build with no broken links. -- [ ] Cross-links to/from sibling pages resolve. -- [ ] Code blocks lint cleanly (correct language tags, runnable where applicable). -- [ ] No prose that duplicates content already covered by another page — link instead. - ---- +- `## Definition of Done` — one `### Task {n}.{m}` block per task, checkbox bullets only: page renders in the local build with no broken links, cross-links to/from sibling pages resolve, code blocks lint cleanly (correct language tags, runnable where applicable), no prose duplicating another page — link instead. +- `## Verify` — build/serve the site locally and load the pages this milestone changed; check their cross-links resolve. Whole-repo gates — a strict full-site build, a pushed branch confirming the CI workflow green, an aggregate `ci` target — run once in `index.md`'s Final Verification, never per milestone. ## Final Verification @@ -76,7 +74,7 @@ Name sections to update (e.g. add `documentation/` to the layout section, distin ## Frontmatter - [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. -- [ ] `sp` equals the sum of per-task SP across milestones. +- [ ] `sp` equals the sum of the milestone files' `sp`. ## Content @@ -85,8 +83,10 @@ Name sections to update (e.g. add `documentation/` to the layout section, distin - [ ] Each page is a milestone task or grouped with siblings under one milestone — no orphan pages. - [ ] DoD bullets are verifiable by loading the rendered page or running the build. - [ ] Every task lists exact file paths. -- [ ] Every milestone has a `Verify` step that includes a build or local-serve check of the pages it changed — no whole-repo gate (strict full-site build, CI-workflow run, aggregate `ci` target); those belong to Final Verification. -- [ ] Each milestone executable from a fresh session with only the plan as context. +- [ ] Every milestone is a file in `milestones/` carrying its own goal, tasks, DoD and Verify — no milestone body in `index.md`. +- [ ] `index.md`'s milestone table has one row per milestone file and matches their frontmatter. +- [ ] Every milestone file's `## Verify` includes a build or local-serve check of the pages that milestone changed — no whole-repo gate (strict full-site build, CI-workflow run, aggregate `ci` target); those belong to `index.md`'s Final Verification. +- [ ] Each milestone file executable from a fresh session with only it and `index.md` as context. ## Documentation hygiene diff --git a/docs/plan_templates/frontend.md b/docs/plan_templates/frontend.md index a75f7a9d..abe27b1c 100644 --- a/docs/plan_templates/frontend.md +++ b/docs/plan_templates/frontend.md @@ -21,23 +21,22 @@ How the new UI fits the component tree, state flow, and data-loading boundaries. ## Milestones -### M1: {Milestone name} — {SP} SP | pending +Generated table — one row per milestone file, projected with `core.plans.milestones.table_columns` by `booping query`. Derived output: no hand-written rows, no milestone bodies in this file. -**Goal**: one sentence — what changes in the UI after this milestone. +## Milestone files -**Verify**: exact commands (scoped tests, a component's typecheck, visual check) or observable outcomes, limited to what this milestone changed. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in Final Verification, never per milestone. +One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: -| Task | Description | Files | SP | Status | -|------|-------------|-------|----|--------| -| 1.1 | ... | `src/components/Foo.tsx` | 2 | pending | +- **Goal** — one sentence directly under the H1: what changes in the UI after this milestone. +- **Scope** — the components, routes and state owners in play and the files this milestone touches, plus where new components mount. +- `## Tasks` — one row per task: -#### Task 1.1 DoD + | Task | Description | Files | SP | Status | + |------|-------------|-------|----|--------| + | 1.1 | ... | `src/components/Foo.tsx` | 2 | pending | -- [ ] Component renders with all documented props. -- [ ] States: loading / empty / error / success covered. -- [ ] Typecheck + test commands pass. - ---- +- `## Definition of Done` — one `### Task {n}.{m}` block per task, checkbox bullets only: component renders with all documented props, loading / empty / error / success states covered, typecheck + test commands pass. +- `## Verify` — exact commands (scoped tests, a component's typecheck, a visual check) or observable outcomes, limited to what this milestone changed. Whole-repo gates (full test suite, repo-wide lint/typecheck, an aggregate `ci` target) run once in `index.md`'s Final Verification, never per milestone. ## Final Verification @@ -73,7 +72,7 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat ## Frontmatter - [ ] `title` matches the plan's H1 and `type` is the task type chosen at intake. -- [ ] `sp` equals the sum of per-task SP across milestones. +- [ ] `sp` equals the sum of the milestone files' `sp`. ## Content @@ -81,8 +80,10 @@ Name sections to update, or state "No CLAUDE.md changes required — {justificat - [ ] DoD bullets are observable in the browser or a test runner. - [ ] Every task lists exact files. - [ ] Every task DoD uses checkboxes, not prose. -- [ ] Every milestone has a `Verify` step scoped to what it changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to Final Verification. -- [ ] Each milestone executable from a fresh session with only the plan as context. +- [ ] Every milestone is a file in `milestones/` carrying its own goal, tasks, DoD and Verify — no milestone body in `index.md`. +- [ ] `index.md`'s milestone table has one row per milestone file and matches their frontmatter. +- [ ] Every milestone file's `## Verify` is scoped to what that milestone changed — no whole-repo gate (full suite, repo-wide lint/typecheck, aggregate `ci` target); those belong to `index.md`'s Final Verification. +- [ ] Each milestone file executable from a fresh session with only it and `index.md` as context. ## Anti-patterns (must be absent) From 169fd72527a6f74ce9188c8517ea7609aeec0e18 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:50:34 +0700 Subject: [PATCH 27/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index d205c7d0..fcb2f555 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -65,7 +65,7 @@ The milestone file is the only place a milestone's work is written down. `index. | --- | --- | --- | --- | | 1 | Scaffold renders tree keys | 3 | done | | 2 | Milestone file contract in config | 3 | done | -| 3 | Plan templates carry the milestone file | 3 | pending | +| 3 | Plan templates carry the milestone file | 3 | done | | 4 | Groom writes milestone files | 3 | pending | | 5 | Milestone state machine and table refresh | 4 | pending | | 6 | Develop delegates paths, not bodies | 4 | pending | @@ -129,7 +129,7 @@ The milestone file is the only place a milestone's work is written down. `index. --- -### M3: Plan templates carry the milestone file — 3 SP | pending +### M3: Plan templates carry the milestone file — 3 SP | done **Goal**: all five plan templates specify a plan as `index.md` plus milestone files, so a drafted plan is written into the new shape by construction. @@ -139,20 +139,20 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 3.1 | In each template's `# Plan Body`, replace the inline `### M1: …` skeleton under `## Milestones` with the generated milestone table, and add a `## Milestone files` section specifying the per-file body shape for that surface (goal, scope and related files, tasks table, per-task DoD, Verify). | `docs/plan_templates/backend.md`, `docs/plan_templates/frontend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md` | 2 | pending | -| 3.2 | Update each `# Quality Checklist`: milestone-shape items point at the milestone file, add an item that the index table matches the milestone files, keep the `sp` rollup item true against per-file `sp`. | same five files | 1 | pending | +| 3.1 | In each template's `# Plan Body`, replace the inline `### M1: …` skeleton under `## Milestones` with the generated milestone table, and add a `## Milestone files` section specifying the per-file body shape for that surface (goal, scope and related files, tasks table, per-task DoD, Verify). | `docs/plan_templates/backend.md`, `docs/plan_templates/frontend.md`, `docs/plan_templates/cli.md`, `docs/plan_templates/claude_skill.md`, `docs/plan_templates/documentation.md` | 2 | done | +| 3.2 | Update each `# Quality Checklist`: milestone-shape items point at the milestone file, add an item that the index table matches the milestone files, keep the `sp` rollup item true against per-file `sp`. | same five files | 1 | done | #### Task 3.1 DoD -- [ ] Every template names the same required frontmatter keys and the same three required headings; surface-specific guidance lives only in the free part of the body. -- [ ] No template still instructs that milestone bodies live in `index.md`. -- [ ] `documentation.md`'s milestone ordering rubric and each template's surface inserts survive the edit. -- [ ] The per-milestone `Verify` rule (scoped, no whole-repo gates) is stated once per template, in the milestone-file section. +- [x] Every template names the same required frontmatter keys and the same three required headings; surface-specific guidance lives only in the free part of the body. +- [x] No template still instructs that milestone bodies live in `index.md`. +- [x] `documentation.md`'s milestone ordering rubric and each template's surface inserts survive the edit. +- [x] The per-milestone `Verify` rule (scoped, no whole-repo gates) is stated once per template, in the milestone-file section. #### Task 3.2 DoD -- [ ] Checklist items are verifiable by reading a plan directory, not by intent. -- [ ] `sp` item reads as the sum of milestone-file `sp` values. +- [x] Checklist items are verifiable by reading a plan directory, not by intent. +- [x] `sp` item reads as the sum of milestone-file `sp` values. --- From ae20bfa8333468165c51f9950e447158b98c71da Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:55:24 +0700 Subject: [PATCH 28/44] feat(groom): draft-plan seeds and writes milestone files draft-plan scaffolds one file per milestone and writes its body in place, then generates index.md's table with booping query; present and cross-review read the same source. sp moves to develop's refresh script. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- playbooks/_partials/plan_templates.md | 9 ++++++--- playbooks/groom/cross-review/opus-5.md | 7 ++----- playbooks/groom/draft-plan/opus-5.md | 27 +++++++++++++++++++++++--- playbooks/groom/draft-plan/prompt.md | 7 ++++--- playbooks/groom/present/sonnet-5.md | 12 ++++++++---- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/playbooks/_partials/plan_templates.md b/playbooks/_partials/plan_templates.md index 0ac611ac..8bac43cd 100644 --- a/playbooks/_partials/plan_templates.md +++ b/playbooks/_partials/plan_templates.md @@ -26,9 +26,12 @@ it — frontmatter (`name`, `description`) plus both top-level sections, generic class: placeholders throughout, no path, milestone or story-point value from this run baked in. Never draft into a bad-fit template, never improvise a shape and name a template after it. -The plan is the run's `index.md`, already carrying its frontmatter and title — the body goes under -them. `sp` and `summary` are yours; write them with: +A plan is two surfaces: the run's `index.md`, already carrying its frontmatter and title, holding the template's top-level sections under them; and one file per milestone, written as [Write the milestone files](#write-the-milestone-files) describes, against the template's milestone-file section. + +`summary` is yours: ``` -booping frontmatter-update {plan}/index.md sp={total} summary="{one line}" +booping frontmatter-update {plan-dir}/index.md summary="{one line}" ``` + +`sp` is not — develop's `refresh-milestone-table` script re-sums it from the milestone files. Never hand-write it. diff --git a/playbooks/groom/cross-review/opus-5.md b/playbooks/groom/cross-review/opus-5.md index fcd05356..e935e8d9 100644 --- a/playbooks/groom/cross-review/opus-5.md +++ b/playbooks/groom/cross-review/opus-5.md @@ -1,9 +1,6 @@ # Cross-review the plan -You are a strict staff engineer reviewing a sprint plan that an AI coding agent will execute from -the plan file alone. Read the plan named in your run-time context — `plans/{slug}/index.md` — and -review it. Find execution gaps, architectural blind spots and rule violations. No generic software -engineering advice: every finding references a specific part of the plan. +You are a strict staff engineer reviewing a sprint plan that an AI coding agent will execute from the plan files alone. Read the plan named in your run-time context — `plans/{slug}/index.md` and every milestone file beside it, `plans/{slug}/{{ config.core.plans.milestones.glob }}` — and review it. Find execution gaps, architectural blind spots and rule violations. No generic software engineering advice: every finding references a specific part of the plan. ## Dimensions @@ -14,7 +11,7 @@ engineering advice: every finding references a specific part of the plan. frameworks or I/O; unclear test boundaries and utility placement. - **Architectural blind spots** — concurrency, state, coupling or data-integrity issues specific to this plan that it does not address. -- **Plan mechanics** — a milestone not executable in a fresh session with only the plan as context; +- **Plan mechanics** — a milestone not executable in a fresh session with only its file and `index.md` as context; a DoD checkbox or `**Verify**` that names nothing observable; a `Files` path that does not exist and is not created by the plan; an out-of-scope item some task targets anyway. diff --git a/playbooks/groom/draft-plan/opus-5.md b/playbooks/groom/draft-plan/opus-5.md index db261507..0d749b61 100644 --- a/playbooks/groom/draft-plan/opus-5.md +++ b/playbooks/groom/draft-plan/opus-5.md @@ -5,8 +5,7 @@ conversation already carries — the blast-radius map and the external ground th - **Draft design with the user**: architecture, pattern choices, data / API / config surface changes, open trade-offs. Iterate until aligned before writing. -- **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) - whose name + description matches the work, then produce the plan against its `# Plan Body`. +- **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) whose name + description matches the work, then produce the plan against its `# Plan Body` — `index.md` from the template's top-level sections, plus one file per milestone. - **Write `summary`**: set the `summary:` frontmatter to a single line of plain plan intent — ≤ ~120 chars / ~20 words, no prose, no trailing period needed. It feeds search and the `sprints.md` snapshot. @@ -14,7 +13,7 @@ conversation already carries — the blast-radius map and the external ground th ## Hard rules - The orchestrator never edits files outside `{project}/plans/`. -- Each milestone executable in a fresh session with only the plan as context. +- Each milestone file executable in a fresh session with only it and `index.md` as context. - Sprint total past **{{ config.core.sprint.default_threshold_sp }} SP** — offer the user a split at a dependency seam (the first slice shippable on its own, each later one useless without it), keep only the first slice that fits the threshold in this plan, and park the rest as sibling stubs to be groomed in their own runs. They @@ -23,4 +22,26 @@ conversation already carries — the blast-radius map and the external ground th {% include "_partials/plan_templates.md" %} +## Write the milestone files + +Yours to write, here, one milestone at a time — no sub-step, no worker agent, no batched pass. `{plan-dir}` is the preamble's `Plan dir:` line. In execution order, per milestone: + +1. Seed the file: + + ``` + booping scaffold core.groom_playbook.milestone_scaffold {plan-dir}/milestones --set id={nn} --set slug={kebab} --set title="{title}" --set sp={sp} --set plan={plan-dir}/index.md + ``` + + `{nn}` is the milestone's position in execution order, zero-padded to two digits; `{kebab}` is its title kebab-cased — the two make the filename. The seed owns everything it writes; never retype it into the body. + +2. Write that milestone's body into the seeded file with a normal file edit, against the chosen template's milestone-file section. Then move to the next milestone. + +`index.md`'s `## Milestones` table is generated from the files on disk, never hand-kept — paste the output of: + +``` +booping query --glob {plan-dir}/{{ config.core.plans.milestones.glob }} --columns {{ config.core.plans.milestones.table_columns | join(',') }} +``` + +A milestone that changes after that is edited in its own file, and the query re-run. + {% include "_partials/sprint_planning.md" %} \ No newline at end of file diff --git a/playbooks/groom/draft-plan/prompt.md b/playbooks/groom/draft-plan/prompt.md index a37a15a2..9aa05943 100644 --- a/playbooks/groom/draft-plan/prompt.md +++ b/playbooks/groom/draft-plan/prompt.md @@ -1,9 +1,10 @@ --- summary: Settle architecture, surface changes and trade-offs with the user, then pick the plan template matching the dominant surface and write the plan - against its Plan Body — milestones, tasks with DoD and Verify, story points - per task / milestone / sprint, `sp` and `summary` frontmatter; verify against - the template's Quality Checklist before returning. + against its Plan Body — `index.md` plus one seeded file per milestone with its + tasks, DoD, Verify and story points, the index's milestone table generated + from those files, and `summary` frontmatter; verify against the template's + Quality Checklist before returning. review_gate: null reviewed_at: 20260731 20:19 --- diff --git a/playbooks/groom/present/sonnet-5.md b/playbooks/groom/present/sonnet-5.md index ef510f0d..20449115 100644 --- a/playbooks/groom/present/sonnet-5.md +++ b/playbooks/groom/present/sonnet-5.md @@ -1,14 +1,18 @@ +Read the plan's milestone rows — never a hand-kept list — with `{plan-dir}` the preamble's `Plan dir:` line: + +``` +booping query --glob {plan-dir}/{{ config.core.plans.milestones.glob }} --columns {{ config.core.plans.milestones.table_columns | join(',') }} +``` + Present user the resulting plan as: ``` Request: {path} Plan: {path} Status: {status} -SPs: {SP total} +SPs: {sum of the rows' sp} -| # | Summary | SP | -| - | ------- | -- | -{one row per milestone — its id, what it delivers, its SP} +{the table the query printed} ## Next Steps From 580a6459e506787d0c0c0f4f4f6729562f399ec0 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 17:55:24 +0700 Subject: [PATCH 29/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index fcb2f555..65bd6339 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -66,7 +66,7 @@ The milestone file is the only place a milestone's work is written down. `index. | 1 | Scaffold renders tree keys | 3 | done | | 2 | Milestone file contract in config | 3 | done | | 3 | Plan templates carry the milestone file | 3 | done | -| 4 | Groom writes milestone files | 3 | pending | +| 4 | Groom writes milestone files | 3 | done | | 5 | Milestone state machine and table refresh | 4 | pending | | 6 | Develop delegates paths, not bodies | 4 | pending | | 7 | Downstream readers and documentation | 3 | pending | @@ -156,7 +156,7 @@ The milestone file is the only place a milestone's work is written down. `index. --- -### M4: Groom writes milestone files — 3 SP | pending +### M4: Groom writes milestone files — 3 SP | done **Goal**: `draft-plan` seeds and writes one file per milestone and renders `index.md`'s table from them; `present` summarizes from the same source. @@ -166,22 +166,22 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 4.1 | Rewrite `draft-plan`'s body: after design alignment, the step itself — no sub-step, no delegation — runs one `booping scaffold` call per milestone to seed the file, then writes that milestone's body into the seeded file with a normal file edit, authored by the step against the chosen template's milestone-file section; then it writes `index.md`'s `## Milestones` table from `booping query`. | `playbooks/groom/draft-plan/prompt.md`, `playbooks/groom/draft-plan/opus-5.md`, `playbooks/_partials/plan_templates.md` | 2 | pending | -| 4.2 | Update `present` and `cross-review` for the multi-file plan: the approval screen's milestone table comes from the same query, and the cross-review briefing names `index.md` plus the milestone files as its read set. | `playbooks/groom/present/sonnet-5.md`, `playbooks/groom/cross-review/opus-5.md` | 1 | pending | +| 4.1 | Rewrite `draft-plan`'s body: after design alignment, the step itself — no sub-step, no delegation — runs one `booping scaffold` call per milestone to seed the file, then writes that milestone's body into the seeded file with a normal file edit, authored by the step against the chosen template's milestone-file section; then it writes `index.md`'s `## Milestones` table from `booping query`. | `playbooks/groom/draft-plan/prompt.md`, `playbooks/groom/draft-plan/opus-5.md`, `playbooks/_partials/plan_templates.md` | 2 | done | +| 4.2 | Update `present` and `cross-review` for the multi-file plan: the approval screen's milestone table comes from the same query, and the cross-review briefing names `index.md` plus the milestone files as its read set. | `playbooks/groom/present/sonnet-5.md`, `playbooks/groom/cross-review/opus-5.md` | 1 | done | #### Task 4.1 DoD -- [ ] The step body states the scaffold invocation once, with `--set` values, and never restates the seeded frontmatter keys. -- [ ] The body-writing mechanism is stated explicitly: scaffold seeds the file, the step writes the body into it, one milestone at a time, with no sub-step and no worker agent involved. -- [ ] `plan_templates.md` no longer says the whole body goes under `index.md`. -- [ ] The index table is described as generated output, with the query invocation given verbatim. -- [ ] The plan's `sp` frontmatter is stated as owned by the refresh script (M5.2), not hand-summed by the step. -- [ ] Milestone filenames are specified as `{nn}-{kebab}.md`, zero-padded, ordered by execution order. +- [x] The step body states the scaffold invocation once, with `--set` values, and never restates the seeded frontmatter keys. +- [x] The body-writing mechanism is stated explicitly: scaffold seeds the file, the step writes the body into it, one milestone at a time, with no sub-step and no worker agent involved. +- [x] `plan_templates.md` no longer says the whole body goes under `index.md`. +- [x] The index table is described as generated output, with the query invocation given verbatim. +- [x] The plan's `sp` frontmatter is stated as owned by the refresh script (M5.2), not hand-summed by the step. +- [x] Milestone filenames are specified as `{nn}-{kebab}.md`, zero-padded, ordered by execution order. #### Task 4.2 DoD -- [ ] `present`'s screen reads milestone rows from the query, not from a hand-kept list. -- [ ] The cross-review briefing's read set names both the index and the milestone files, and its return contract is unchanged. +- [x] `present`'s screen reads milestone rows from the query, not from a hand-kept list. +- [x] The cross-review briefing's read set names both the index and the milestone files, and its return contract is unchanged. --- From 8f9b4e4629a93f5e187915fbbeae2478f1611f3d Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 18:10:10 +0700 Subject: [PATCH 30/44] feat(develop): milestone status is run state, table is generated A milestone state machine addressed per instance writes status via playbook-transition only; each edge runs refresh-milestone-table, which rewrites index.md's Milestones table and re-sums the plan's sp from the milestone files. develop-loop moves into a one-step milestones subgraph, which is what makes the {instance} artifact legal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../scripts/refresh_milestone_table_test.py | 167 ++++++++++++++++++ .../develop/_scripts/refresh-milestone-table | 110 ++++++++++++ playbooks/develop/playbook.yaml | 38 +++- 3 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 booping-python/tests/scripts/refresh_milestone_table_test.py create mode 100755 playbooks/develop/_scripts/refresh-milestone-table diff --git a/booping-python/tests/scripts/refresh_milestone_table_test.py b/booping-python/tests/scripts/refresh_milestone_table_test.py new file mode 100644 index 00000000..edc1034e --- /dev/null +++ b/booping-python/tests/scripts/refresh_milestone_table_test.py @@ -0,0 +1,167 @@ +"""Tests for the `develop` playbook's milestone-table hook script. + +The script shells back into `booping`, so it is exercised as a subprocess the way +`booping playbook-transition` runs it — the plan directory as argv, inside a project +whose vault config can redirect the milestone glob and columns. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + +SCRIPT = ( + Path(__file__).resolve().parents[3] + / "playbooks" + / "develop" + / "_scripts" + / "refresh-milestone-table" +) + +INDEX = """--- +title: Demo +sp: 99 +status: in-progress +--- + +# Demo + +## Context + +Prose that must survive | pipes and all. + +## Milestones + +| id | title | +| --- | --- | +| 01 | stale row | + +## Final Verification + +`just ci` green. +""" + +MILESTONES = { + "01-first-thing.md": ('"01"', "First thing", 3, "pending"), + "02-second.md": ('"02"', "Second", 2, "done"), +} + + +def _project(tmp_path: Path, *, config: str = "", milestone_dir: str = "milestones") -> Path: + repo = tmp_path / "repo" + (repo / "vault").mkdir(parents=True) + (repo / ".booping").write_text("project_name: demo\nvault_path: vault\n") + if config: + (repo / "vault" / "config.yaml").write_text(config) + + plan = repo / "vault" / "plans" / "demo" + (plan / milestone_dir).mkdir(parents=True) + (plan / "index.md").write_text(INDEX) + for name, (ident, title, sp, status) in MILESTONES.items(): + (plan / milestone_dir / name).write_text( + f"---\nid: {ident}\ntitle: {title}\nsp: {sp}\nstatus: {status}\n" + f"plan: plans/demo/index.md\n---\n\n# {title}\n" + ) + return plan + + +def _run(plan: Path, tmp_path: Path) -> subprocess.CompletedProcess[str]: + env = dict(os.environ, XDG_CONFIG_HOME=str(tmp_path / "xdg")) + return subprocess.run( + [str(SCRIPT), str(plan)], env=env, capture_output=True, text=True, check=False + ) + + +def _section(text: str, heading: str) -> str: + body = text.split(f"\n{heading}\n", 1)[1] + return body.split("\n## ", 1)[0].strip() + + +def test_renders_one_row_per_milestone_file(tmp_path: Path) -> None: + plan = _project(tmp_path) + result = _run(plan, tmp_path) + + assert result.returncode == 0, result.stderr + assert _section((plan / "index.md").read_text(), "## Milestones") == ( + "| id | title | sp | status |\n" + "| --- | --- | --- | --- |\n" + "| 01 | First thing | 3 | pending |\n" + "| 02 | Second | 2 | done |" + ) + + +def test_leaves_the_surrounding_sections_untouched(tmp_path: Path) -> None: + plan = _project(tmp_path) + _run(plan, tmp_path) + + text = (plan / "index.md").read_text() + assert _section(text, "## Context") == "Prose that must survive | pipes and all." + assert _section(text, "## Final Verification") == "`just ci` green." + assert text.startswith("---\ntitle: Demo\n") + + +def test_stamps_sp_with_the_sum_of_the_milestone_files(tmp_path: Path) -> None: + plan = _project(tmp_path) + _run(plan, tmp_path) + + assert "\nsp: 5\n" in (plan / "index.md").read_text() + + +def test_writes_the_table_into_a_section_that_has_none(tmp_path: Path) -> None: + plan = _project(tmp_path) + (plan / "index.md").write_text( + INDEX.replace("| id | title |\n| --- | --- |\n| 01 | stale row |\n\n", "") + ) + result = _run(plan, tmp_path) + + assert result.returncode == 0, result.stderr + assert "| 01 | First thing | 3 | pending |" in _section( + (plan / "index.md").read_text(), "## Milestones" + ) + + +def test_second_run_changes_nothing(tmp_path: Path) -> None: + plan = _project(tmp_path) + _run(plan, tmp_path) + once = (plan / "index.md").read_text() + _run(plan, tmp_path) + + assert (plan / "index.md").read_text() == once + + +def test_follows_the_glob_and_columns_the_config_declares(tmp_path: Path) -> None: + plan = _project( + tmp_path, + milestone_dir="stages", + config="core:\n plans:\n milestones:\n" + " glob: stages/*.md\n table_columns: [title, status]\n", + ) + result = _run(plan, tmp_path) + + assert result.returncode == 0, result.stderr + assert _section((plan / "index.md").read_text(), "## Milestones") == ( + "| title | status |\n" + "| --- | --- |\n" + "| First thing | pending |\n" + "| Second | done |" + ) + + +@pytest.mark.parametrize("removed", ["milestones", "index.md"]) +def test_reports_the_plan_dir_and_writes_nothing_when_a_piece_is_missing( + tmp_path: Path, removed: str +) -> None: + plan = _project(tmp_path) + before = (plan / "index.md").read_text() + for path in sorted((plan / removed).rglob("*"), reverse=True) + [plan / removed]: + path.rmdir() if path.is_dir() else path.unlink() + + result = _run(plan, tmp_path) + + assert result.returncode != 0 + assert str(plan) in result.stderr + if removed != "index.md": + assert (plan / "index.md").read_text() == before diff --git a/playbooks/develop/_scripts/refresh-milestone-table b/playbooks/develop/_scripts/refresh-milestone-table new file mode 100755 index 00000000..b94b6e23 --- /dev/null +++ b/playbooks/develop/_scripts/refresh-milestone-table @@ -0,0 +1,110 @@ +#!/usr/bin/env -S uv run --script --quiet +# /// script +# requires-python = ">=3.11" +# dependencies = ["pyyaml"] +# /// +"""Hook: regenerate a plan's `## Milestones` table from its milestone files. + +Used by the `develop` playbook on every `milestone` transition, so `index.md`'s table +and the plan's `sp` are projections of the milestone files rather than hand-kept prose. + +Usage: `refresh-milestone-table [PLAN_DIR]` — the plan directory defaults to +`BOOPING_WORKDIR`, which `booping playbook-transition` sets to the run workdir. + +The glob and the table's columns come from `core.plans.milestones`; the rows come from +`booping query` over the plan directory. Only the block between the `## Milestones` +heading and the next heading is rewritten, plus the `sp:` frontmatter line — after +grooming this script is the only writer of the plan's `sp`. +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +import yaml + +BOOPING = Path(__file__).resolve().parents[3] / "bin" / "booping" +CONFIG_PATH = "core.plans.milestones" +FRONTMATTER = re.compile(r"\A(---\n)(.*?)(\n---\n)", re.DOTALL) +SECTION = re.compile(r"^## Milestones[ \t]*$", re.MULTILINE) +HEADING = re.compile(r"^#{1,6} ", re.MULTILINE) +SP_LINE = re.compile(r"^sp:.*$", re.MULTILINE) + + +def _booping(args: list[str], cwd: Path) -> str: + result = subprocess.run( + [str(BOOPING), *args], cwd=cwd, capture_output=True, text=True, check=False + ) + if result.returncode != 0: + sys.exit(f"refresh-milestone-table: booping {args[0]} failed: {result.stderr.strip()}") + return result.stdout + + +def cell(value: Any) -> str: + if value is None: + return "" + return str(value).replace("|", "\\|").replace("\n", " ") + + +def table(rows: list[dict[str, Any]], columns: list[str]) -> str: + lines = [ + "| " + " | ".join(columns) + " |", + "| " + " | ".join("---" for _ in columns) + " |", + ] + lines += ["| " + " | ".join(cell(row.get(col)) for col in columns) + " |" for row in rows] + return "\n".join(lines) + "\n" + + +def replace_section(text: str, rendered: str, index: Path) -> str: + match = SECTION.search(text) + if match is None: + sys.exit(f"refresh-milestone-table: no `## Milestones` heading in {index}") + body_start = match.end() + nxt = HEADING.search(text, body_start + 1) + body_end = nxt.start() if nxt else len(text) + return text[:body_start] + f"\n\n{rendered}\n" + text[body_end:] + + +def restamp_sp(text: str, total: int, index: Path) -> str: + match = FRONTMATTER.match(text) + if match is None or not SP_LINE.search(match.group(2)): + sys.exit(f"refresh-milestone-table: no `sp:` frontmatter key in {index}") + head = SP_LINE.sub(f"sp: {total}", match.group(2), count=1) + return match.group(1) + head + match.group(3) + text[match.end() :] + + +def main() -> None: + raw = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("BOOPING_WORKDIR", ".") + plan = Path(raw).expanduser().resolve() + index = plan / "index.md" + if not index.is_file(): + sys.exit(f"refresh-milestone-table: no index.md in {plan}") + + spec = yaml.safe_load(_booping(["config-get", CONFIG_PATH], plan)) or {} + globs = spec["glob"] + globs = globs if isinstance(globs, list) else [globs] + columns = [str(col) for col in spec["table_columns"]] + + query = ["query", "--project", str(plan), "--columns", ",".join(columns), "--output", "json"] + for pattern in globs: + query += ["--glob", str(pattern)] + rows: list[dict[str, Any]] = json.loads(_booping(query, plan)) + if not rows: + sys.exit(f"refresh-milestone-table: no milestone files in {plan}") + + total = sum(row["sp"] for row in rows if isinstance(row.get("sp"), int)) + text = index.read_text() + updated = restamp_sp(replace_section(text, table(rows, columns), index), total, index) + if updated != text: + index.write_text(updated) + print(f"refresh-milestone-table: {len(rows)} milestones, sp={total} → {index}") + + +if __name__ == "__main__": + main() diff --git a/playbooks/develop/playbook.yaml b/playbooks/develop/playbook.yaml index bbd470ae..4916472f 100644 --- a/playbooks/develop/playbook.yaml +++ b/playbooks/develop/playbook.yaml @@ -2,8 +2,13 @@ state: run graph: intake: [] provision: [intake] - develop-loop: [provision] - verify: [develop-loop] + milestones: + dependencies: [provision] + state: milestone + repeat: once per milestone file in the plan's `milestones/`, in `id` order + graph: + develop-loop: [] + verify: [milestones] wrap-up: [verify] states: @@ -71,3 +76,32 @@ states: - done - fail - cancelled + + milestone: + artifact: milestones/{instance}.md + initial: pending + statuses: + pending: + transitions: + - to: in-progress + when: "the milestone's group is being handed to a worker" + hooks: + - "script refresh-milestone-table" + in-progress: + transitions: + - to: done + when: "the worker reported and every checkbox under the milestone file's `## Definition of Done` is `[x]`" + hooks: + - "script refresh-milestone-table" + - to: blocked + when: "a `**Blocked (n/2)**` line under the milestone file's `## Notes` records a failed attempt" + hooks: + - "script refresh-milestone-table" + blocked: + transitions: + - to: in-progress + when: "the next attempt on the same milestone starts" + hooks: + - "script refresh-milestone-table" + done: + terminal: true From c2e7bfe2aa76c5b2493eb90d94638413a5f1bd0e Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 18:10:10 +0700 Subject: [PATCH 31/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index 65bd6339..dc2ab62e 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -67,7 +67,7 @@ The milestone file is the only place a milestone's work is written down. `index. | 2 | Milestone file contract in config | 3 | done | | 3 | Plan templates carry the milestone file | 3 | done | | 4 | Groom writes milestone files | 3 | done | -| 5 | Milestone state machine and table refresh | 4 | pending | +| 5 | Milestone state machine and table refresh | 4 | done | | 6 | Develop delegates paths, not bodies | 4 | pending | | 7 | Downstream readers and documentation | 3 | pending | | 8 | Reports, structure checks and eval fixtures | 2 | pending | @@ -185,7 +185,7 @@ The milestone file is the only place a milestone's work is written down. `index. --- -### M5: Milestone state machine and table refresh — 4 SP | pending +### M5: Milestone state machine and table refresh — 4 SP | done **Goal**: milestone status is persisted run state written only by `booping playbook-transition`, and every transition refreshes `index.md`'s milestone table. @@ -195,27 +195,29 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 5.1 | Add the `milestone` states entry to develop — artifact `milestones/{instance}.md`, initial `pending`, transitions `pending → in-progress → done`, `in-progress → blocked`, `blocked → in-progress`, each carrying the table-refresh hook. The `blocked` edge's `when` names the `**Blocked (n/2)**` attempt line develop-loop already writes, relocated into the milestone file's `## Notes`; the two-attempt abort rule stays develop-loop's prose and does not become a gate. | `playbooks/develop/playbook.yaml` | 2 | pending | -| 5.2 | Write `refresh-milestone-table` — a uv-inline Python hook script taking the plan dir, querying `core.plans.milestones`, replacing the `## Milestones` table in `index.md` in place and re-stamping the plan's `sp` frontmatter to the sum of milestone-file `sp` — with tests run by `just pytest` that invoke the script as a subprocess against a tmp plan dir. | `playbooks/develop/_scripts/refresh-milestone-table`, `booping-python/tests/scripts/refresh_milestone_table_test.py` | 2 | pending | +| 5.1 | Add the `milestone` states entry to develop — artifact `milestones/{instance}.md`, initial `pending`, transitions `pending → in-progress → done`, `in-progress → blocked`, `blocked → in-progress`, each carrying the table-refresh hook. The `blocked` edge's `when` names the `**Blocked (n/2)**` attempt line develop-loop already writes, relocated into the milestone file's `## Notes`; the two-attempt abort rule stays develop-loop's prose and does not become a gate. | `playbooks/develop/playbook.yaml` | 2 | done | +| 5.2 | Write `refresh-milestone-table` — a uv-inline Python hook script taking the plan dir, querying `core.plans.milestones`, replacing the `## Milestones` table in `index.md` in place and re-stamping the plan's `sp` frontmatter to the sum of milestone-file `sp` — with tests run by `just pytest` that invoke the script as a subprocess against a tmp plan dir. | `playbooks/develop/_scripts/refresh-milestone-table`, `booping-python/tests/scripts/refresh_milestone_table_test.py` | 2 | done | #### Task 5.1 DoD -- [ ] `status:` on a milestone file is written by `booping playbook-transition` only — no prompt instructs a hand-edit. -- [ ] Each transition's `when` is stated against observable milestone-file content, and `blocked` records the attempt count the loop already tracks. -- [ ] `booping playbook-state develop --workdir {plan}` reports one row per milestone instance alongside the run machine. -- [ ] The existing `run` machine's statuses and hooks are untouched. +- [x] `status:` on a milestone file is written by `booping playbook-transition` only — no prompt instructs a hand-edit. +- [x] Each transition's `when` is stated against observable milestone-file content, and `blocked` records the attempt count the loop already tracks. +- [x] `booping playbook-state develop --workdir {plan}` reports one row per milestone instance alongside the run machine. +- [x] The existing `run` machine's statuses and hooks are untouched. #### Task 5.2 DoD -- [ ] The script rewrites only the block between the `## Milestones` heading and the next heading; surrounding prose is byte-identical. -- [ ] The plan's `sp` frontmatter equals the sum of milestone-file `sp` after every run — this is the only writer of `sp` after grooming. -- [ ] Running it twice in a row produces no diff the second time. -- [ ] Columns and glob come from `core.plans.milestones`, never hard-coded. -- [ ] A plan with no `milestones/` directory exits non-zero with a message naming the plan dir, and writes nothing. -- [ ] Tests run under `just pytest` and drive the script as a subprocess against a tmp plan dir. +- [x] The script rewrites only the block between the `## Milestones` heading and the next heading; surrounding prose is byte-identical. +- [x] The plan's `sp` frontmatter equals the sum of milestone-file `sp` after every run — this is the only writer of `sp` after grooming. +- [x] Running it twice in a row produces no diff the second time. +- [x] Columns and glob come from `core.plans.milestones`, never hard-coded. +- [x] A plan with no `milestones/` directory exits non-zero with a message naming the plan dir, and writes nothing. +- [x] Tests run under `just pytest` and drive the script as a subprocess against a tmp plan dir. --- +**Note**: the `{instance}` artifact needed a subgraph to reference the machine (engine gate at `booping-python/src/booping/context/playbook.py:405`), so `develop-loop` now sits in a one-step `milestones` subgraph repeating once per milestone file — user-confirmed. `booping playbook-state` keys instances as `01-demo.md`, `.md` included; cosmetic, left as is. + ### M6: Develop delegates paths, not bodies — 4 SP | pending **Goal**: provision groups from milestone frontmatter, develop-loop briefs the worker with paths, and the worker reads its milestone file itself. From 5b7733b46674d6dd0985546593fa9222c5a6fab6 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 18:15:48 +0700 Subject: [PATCH 32/44] feat(develop): brief workers with milestone paths, not bodies develop-loop composes one literal briefing block naming the milestone files as contract and index.md as context; provision groups from a query over milestone frontmatter and verify reads the milestone files. The worker contract states which path binds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- playbooks/develop/develop-loop/base.md | 92 +++++++++++++--------- playbooks/develop/provision/base.md | 17 ++-- playbooks/develop/verify/base.md | 13 ++- src/templates/_partials/_developer_body.j2 | 13 +-- 4 files changed, 80 insertions(+), 55 deletions(-) diff --git a/playbooks/develop/develop-loop/base.md b/playbooks/develop/develop-loop/base.md index 2a46ee30..f43c625d 100644 --- a/playbooks/develop/develop-loop/base.md +++ b/playbooks/develop/develop-loop/base.md @@ -1,58 +1,76 @@ -# Run the sprint, group by group +# Run the sprint, milestone by milestone -Milestone groups run **sequentially** — never two workers on one sprint branch, and never edit -application code yourself. Don't use git worktrees. +This step runs once per milestone file, in `id` order; the instance's milestone is the one its `--instance` names. Provision's groups decide only how briefings are batched: a group's **first** instance composes and delegates one briefing covering that whole group, and the group's later instances ride that briefing straight to their own close. -Provision already fired the `ready-for-dev` → `in-progress` edge. On a resume that still finds the -plan at `ready-for-dev`, take that edge before the first delegation; otherwise never touch it. +Milestones run **sequentially** — never two workers on one sprint branch, and never edit application code yourself. Don't use git worktrees. -For each confirmed milestone group, in order: +Provision already fired the `ready-for-dev` → `in-progress` edge. On a resume that still finds the plan at `ready-for-dev`, take that edge before the first delegation; otherwise never touch it. + +Milestone transitions are the `## State` section's `milestone` machine, invoked as: + +``` +booping playbook-transition develop {to} --state milestone --instance {nn}-{kebab} --workdir {plan-dir} +``` + +`{nn}-{kebab}` is the milestone file's name without `.md`. The edge's hook regenerates `index.md`'s `## Milestones` table and its `sp` — never edit either by hand. + +## Delegating a group 1. Open one tracking task for the group. -2. Compose **one** briefing covering every milestone in the group: per-milestone request, related - files, DoD and Verify, plus the project conventions and the plan's scope boundary. Briefings - carry no lesson paths — the worker gets its lesson context from its own extension file. -3. Delegate the briefing to the worker agent named in [Available Agents](#available-agents) — - always delegate, even for a one-line change. -4. Do not continue next milestone in the same agent by resurrecting it with ID. Always start a fresh agent with empty context. -5. When the worker reports done, for **each milestone** in the group: - - Verify the output against the milestone's DoD and the resulting diff. - - Run the milestone's plan-authored `Verify` command — the project's own guardrails all wait for - `verify` at sprint end. - - Flip each completed task's DoD checkboxes in the plan: `- [ ]` → `- [x]`. - - Flip each task row in the milestone's status table: `pending` → `done`. - - Flip the milestone status to `done`. - - Commit in the attached repo, one commit per milestone, message format - `{{ config.core.develop_playbook.git.commit_message }}`. -5. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then - `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. -6. Report group completion to the user with a one-paragraph summary (what shipped, anything - deferred) before starting the next group. - -Plan edits here are bookkeeping only: no new milestones, no rewritten tasks, and never `status:`, -which the run machine owns. +2. Transition every milestone in the group onto the edge whose `when` is the group being handed to a worker. +3. Compose **one** briefing, exactly this block: + + ```markdown + ## Task + + Implement the milestones below, in the order given, on branch `{branch}`. + + ## Inputs + + - Contract — `{plan-dir}/milestones/{nn}-{kebab}.md`: one line per milestone in the group, in order. + - Context — `{plan-dir}/index.md`: scope boundary, architecture and decisions. + - Conventions — {the repo's `CLAUDE.md`, plus any convention file a milestone names}. + - Drift — {intake's outstanding findings, or `none`}. + + ## Return + + One block per milestone, in the same order: what was done in one paragraph, then the files touched. No diffs, no pasted code, no command logs. + ``` + + Paths only: never paste a milestone's goal, tasks, DoD or Verify text into the briefing — the worker reads its contract itself. Briefings carry no lesson paths either; the worker gets its lesson context from its own extension file. +4. Delegate the briefing to the worker agent named in [Available Agents](#available-agents) — always delegate, even for a one-line change. +5. Never resurrect a worker by ID for the next group. Each group gets a fresh agent with empty context. + +## Closing a milestone + +Once the briefing that covers this instance's milestone has come back: + +1. Verify the diff against the milestone file's `## Definition of Done`. +2. Run the milestone file's `## Verify` command — the project's own guardrails all wait for `verify` at sprint end. +3. In the milestone file: flip each satisfied DoD checkbox `- [ ]` → `- [x]`, and each finished task row's status. Bookkeeping only — no new tasks, no rewritten ones, and never `status:`, which the machine owns. +4. Take the milestone's closing edge. +5. Commit in the attached repo, one commit per milestone, message format `{{ config.core.develop_playbook.git.commit_message }}`. +6. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. +7. Report to the user in one paragraph — what shipped, anything deferred — before the next milestone starts. ## When a milestone does not close -A failing `Verify` or a wrong diff goes back to the worker as a fix briefing, and the attempt is -recorded under the milestone in the plan: +A failing `Verify` or a wrong diff goes back to the worker as a fix briefing — the same block, the same contract path — and the attempt is recorded under the milestone file's `## Notes`: **Blocked (1/2)**: `{verify command}` failed on {what failed}; re-briefed the worker to {fix}. -After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve -the abort, then take the `in-progress` → `fail` edge. No scope additions and no runner-authored fix -at any point. +Take the milestone's blocked edge on the record, and the edge back when the next attempt starts. After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve the abort, then take the run machine's `in-progress` → `fail` edge. No scope additions and no runner-authored fix at any point. ## Return format ``` ## Changed: -- [UPDATED] plans/{slug}/index.md — {groups closed, milestones flipped} -- repo commits: {one line per milestone commit} +- [UPDATED] plans/{slug}/milestones/{nn}-{kebab}.md — {status before} → {status after}, {n} DoD checkboxes flipped +- repo commit: {the milestone's commit message} ## Notes: -- {per group: what shipped, anything deferred} -- {the Verify verdict per milestone, and any fix attempts spent} +- briefing: {the group this milestone's briefing covered, or that it rode an earlier group's briefing} +- verify: {the milestone's Verify verdict, and any fix attempts spent} ``` diff --git a/playbooks/develop/provision/base.md b/playbooks/develop/provision/base.md index ec274df4..bca10ad2 100644 --- a/playbooks/develop/provision/base.md +++ b/playbooks/develop/provision/base.md @@ -4,8 +4,8 @@ Set the sprint up in one step: a confirmed branch to commit on, and the milestone groups every briefing in `develop-loop` will cover. -You get the plan — its type, title, slug, and its milestones with story points and execution -order — the repo's current branch, and the drift findings intake raised. +You get the plan — its type, title and slug — the repo's current branch, and the drift findings +intake raised. The milestones come off disk, one file each. ## Branch @@ -17,6 +17,12 @@ In multi-repo projects, reuse the same branch name across repos unless the user ## Milestone groups +Enumerate the milestones in execution order — never from `index.md`'s table, which is derived: + +``` +booping query --glob {plan-dir}/{{ config.core.plans.milestones.glob }} --columns {{ config.core.plans.milestones.table_columns | join(',') }} --sort id +``` + {% if config.core.sprint.max_milestones_per_agent -%} Group consecutive milestones into agent briefings: each briefing covers **up to {{ config.core.sprint.max_milestones_per_agent }} milestone(s)**. Group only when the milestones share enough context that one agent handling them in sequence is @@ -28,9 +34,10 @@ keep them one-per-briefing. {%- endif %} The groups are yours to settle — reported in the return, never put to the user for confirmation. -Settle them as a table you keep for the return: +Settle them as a table you keep for the return, milestones named by file so `develop-loop` briefs +paths: -| Group | Milestones | SP | Grouped because | +| Group | Milestone files | SP | Grouped because | | --- | --- | --- | --- | Carry intake's outstanding drift alongside them, so `develop-loop` briefs against it. @@ -50,7 +57,7 @@ With the branch created and the groups settled, advance the run per the `## Stat - branch: `{name}` created off the current branch `{base}`, name confirmed by the user - transition: {the transition report verbatim} -- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 M1+M2, G2 M3, G3 M4+M5 +- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 `01-{kebab}`+`02-{kebab}`, G2 `03-{kebab}` - drift: {what intake raised and whether anything is outstanding} ``` diff --git a/playbooks/develop/verify/base.md b/playbooks/develop/verify/base.md index ef2f8851..32abbca6 100644 --- a/playbooks/develop/verify/base.md +++ b/playbooks/develop/verify/base.md @@ -31,8 +31,7 @@ Verification alone. ## Plan bookkeeping -Read the plan off disk and check what the loop actually wrote, not what it reported: every task -DoD checkbox `[x]`, every milestone status `done`. Each is its own check. +Read the milestone files off disk — `plans/{slug}/{{ config.core.plans.milestones.glob }}` — and check what the loop actually wrote, not what it reported: every task DoD checkbox `[x]`, every milestone file's `status:` `done`. Each is its own check. `index.md`'s `## Milestones` table and its `sp` are derived from the milestone files by a transition hook, so neither is evidence of anything. ## Artifact @@ -42,10 +41,10 @@ artifact. ## Return format ```markdown -- `<command>`[ (plan Final Verification)] — PASS|FAIL — <evidence> -- skipped: <tool or config file> — hook-enforced, fired at commit time -- plan: DoD checkboxes — PASS|FAIL — <n of m [x], else the unticked ones by task> -- plan: milestone statuses — PASS|FAIL — <all done, else the milestones still open> -- verdict: green|red — <n> of <m> commands passing|failing, <n> of 2 plan checks passing|failing +- `{command}`[ (plan Final Verification)] — PASS|FAIL — {evidence} +- skipped: {tool or config file} — hook-enforced, fired at commit time +- milestones: DoD checkboxes — PASS|FAIL — {n of m [x], else the unticked ones by milestone file and task} +- milestones: statuses — PASS|FAIL — {all done, else the milestone files still open} +- verdict: green|red — {n} of {m} commands passing|failing, {n} of 2 milestone checks passing|failing ``` \ No newline at end of file diff --git a/src/templates/_partials/_developer_body.j2 b/src/templates/_partials/_developer_body.j2 index e52a5566..5da2c2bb 100644 --- a/src/templates/_partials/_developer_body.j2 +++ b/src/templates/_partials/_developer_body.j2 @@ -1,16 +1,17 @@ You're an experienced developer. Implement the milestone(s) in the briefing the orchestrator hands you. -The briefing carries the request, related files, and the definition of done. A briefing may cover one milestone or a small group of consecutive milestones — implement them in the order given. +The briefing names paths, never pasted content. Its `## Inputs` block gives you a **Contract** line per milestone — that milestone file is the authoritative statement of goal, tasks, files, definition of done and verification — and one **Context** line, the plan's `index.md`, which fixes the scope boundary and never overrides a contract. A briefing may cover one milestone or a small group of consecutive milestones. ## Workflow -1. Skim the **related files** in the briefing — read the ones you'll touch; treat the rest as context if helpful. -2. Implement exactly what the briefing specifies, milestone by milestone in the given order. No extras. No "while I'm here" refactors. +1. Read every contract file in full, then the parts of the context file its milestones reference. +2. Implement exactly what the contracts specify, milestone by milestone in the order given. No extras. No "while I'm here" refactors. 3. Report back using the format below. The orchestrator runs the verification commands after you report. ## Hard rules -- Stay within the **related files** listed in the briefing. If a file outside that set needs to change, stop and report — do not silently expand scope. +- Stay within the files the contract lists. If a file outside that set needs to change, stop and report — do not silently expand scope. +- Never write to the plan directory: checkbox flips and status writes are the orchestrator's. - Never add error handling, validation, or logging beyond what the task specifies. - Never produce changes not directly connected to the request. - No comments explaining what the code does. Comments only for non-obvious WHY. @@ -24,8 +25,8 @@ The briefing carries the request, related files, and the definition of done. A b Signal completion to the orchestrator with a brief per-milestone summary and the files you changed: ~~~markdown -## Milestone <id or title> -<one short paragraph: what was done, and any unrelated greens-blockers fixed along the way> +## Milestone {id or title} +{one short paragraph: what was done, and any unrelated greens-blockers fixed along the way} Files touched: - path/to/file From b8b0afb43fba8827f736fd605f13cf183d2356bd Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 18:15:48 +0700 Subject: [PATCH 33/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index dc2ab62e..46ca6152 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -68,7 +68,7 @@ The milestone file is the only place a milestone's work is written down. `index. | 3 | Plan templates carry the milestone file | 3 | done | | 4 | Groom writes milestone files | 3 | done | | 5 | Milestone state machine and table refresh | 4 | done | -| 6 | Develop delegates paths, not bodies | 4 | pending | +| 6 | Develop delegates paths, not bodies | 4 | done | | 7 | Downstream readers and documentation | 3 | pending | | 8 | Reports, structure checks and eval fixtures | 2 | pending | @@ -218,7 +218,7 @@ The milestone file is the only place a milestone's work is written down. `index. **Note**: the `{instance}` artifact needed a subgraph to reference the machine (engine gate at `booping-python/src/booping/context/playbook.py:405`), so `develop-loop` now sits in a one-step `milestones` subgraph repeating once per milestone file — user-confirmed. `booping playbook-state` keys instances as `01-demo.md`, `.md` included; cosmetic, left as is. -### M6: Develop delegates paths, not bodies — 4 SP | pending +### M6: Develop delegates paths, not bodies — 4 SP | done **Goal**: provision groups from milestone frontmatter, develop-loop briefs the worker with paths, and the worker reads its milestone file itself. @@ -228,30 +228,32 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 6.1 | Rewrite `develop-loop`'s briefing composition: the briefing carries the plan `index.md` path, the group's milestone file paths, the branch and project conventions, and a bounded return contract — never a pasted milestone body; bookkeeping becomes flipping DoD checkboxes in the milestone file plus the milestone transition. | `playbooks/develop/develop-loop/base.md` | 2 | pending | -| 6.2 | Update `provision` to enumerate and group milestones from `booping query` over the milestone files, and `verify` to check every milestone file's DoD checkboxes and `status: done` rather than reading `index.md`'s body. | `playbooks/develop/provision/base.md`, `playbooks/develop/verify/base.md` | 1 | pending | -| 6.3 | Update the worker agent contract: the briefing names paths, the agent reads the milestone file as its authoritative contract and the plan index for scope boundary, and its report shape stays bounded to what the runner needs. | `src/templates/agents/booping-developer.md.j2` | 1 | pending | +| 6.1 | Rewrite `develop-loop`'s briefing composition: the briefing carries the plan `index.md` path, the group's milestone file paths, the branch and project conventions, and a bounded return contract — never a pasted milestone body; bookkeeping becomes flipping DoD checkboxes in the milestone file plus the milestone transition. | `playbooks/develop/develop-loop/base.md` | 2 | done | +| 6.2 | Update `provision` to enumerate and group milestones from `booping query` over the milestone files, and `verify` to check every milestone file's DoD checkboxes and `status: done` rather than reading `index.md`'s body. | `playbooks/develop/provision/base.md`, `playbooks/develop/verify/base.md` | 1 | done | +| 6.3 | Update the worker agent contract: the briefing names paths, the agent reads the milestone file as its authoritative contract and the plan index for scope boundary, and its report shape stays bounded to what the runner needs. | `src/templates/agents/booping-developer.md.j2` | 1 | done | #### Task 6.1 DoD -- [ ] The briefing spec lists exactly: plan index path, milestone file paths, branch, conventions, return contract — written out as the literal briefing block the loop composes, so the shape is fixed rather than described. -- [ ] No instruction to inline goal, tasks, DoD or Verify text into the briefing survives. -- [ ] Milestone status flips are stated as the transition invocation with `--state milestone --instance`, and checkbox flips are stated against the milestone file. -- [ ] The one-worker-at-a-time and fresh-agent-per-group rules survive unchanged. +- [x] The briefing spec lists exactly: plan index path, milestone file paths, branch, conventions, return contract — written out as the literal briefing block the loop composes, so the shape is fixed rather than described. +- [x] No instruction to inline goal, tasks, DoD or Verify text into the briefing survives. +- [x] Milestone status flips are stated as the transition invocation with `--state milestone --instance`, and checkbox flips are stated against the milestone file. +- [x] The one-worker-at-a-time and fresh-agent-per-group rules survive unchanged. #### Task 6.2 DoD -- [ ] Provision's grouping table is fed by the query, and `core.sprint.max_milestones_per_agent` still bounds a group. -- [ ] Verify reads the milestone files and treats `index.md`'s table as derived. +- [x] Provision's grouping table is fed by the query, and `core.sprint.max_milestones_per_agent` still bounds a group. +- [x] Verify reads the milestone files and treats `index.md`'s table as derived. #### Task 6.3 DoD -- [ ] The rendered agent body tells the worker which path is the contract and which is context, against the same briefing block M6.1 fixes — no new invocation flag or YAML key is introduced, only the briefing's `## Inputs` lines change. -- [ ] The worker is still barred from vault writes — checkbox and status writes stay the runner's. -- [ ] The report format stays a bounded per-milestone block. +- [x] The rendered agent body tells the worker which path is the contract and which is context, against the same briefing block M6.1 fixes — no new invocation flag or YAML key is introduced, only the briefing's `## Inputs` lines change. +- [x] The worker is still barred from vault writes — checkbox and status writes stay the runner's. +- [x] The report format stays a bounded per-milestone block. --- +**Note**: 6.3 landed in `src/templates/_partials/_developer_body.j2`, the only body `agents/booping-developer.md.j2` includes. + ### M7: Downstream readers and documentation — 3 SP | pending **Goal**: every surface that reads a plan "in full" follows the milestone files, and the hand-authored docs describe the new shape. From 88be9067e5bf57f1adff7116ae7bed5c1cb77b8f Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 19:10:30 +0700 Subject: [PATCH 34/44] docs: 202608101223-docs-refresh leftovers Uncommitted output of the docs-refresh run, captured before the milestone-file sprint rewrites the same surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- README.md | 46 +++++++------- documentation/code_review.md | 12 ++-- documentation/develop.md | 20 +++--- documentation/groom.md | 17 +++--- documentation/index.md | 18 +++--- documentation/playbook.md | 61 ++++++++----------- documentation/project_config.md | 52 +++++++++------- documentation/vault.md | 36 +++++------ mkdocs.yml | 2 +- vault/_lessons/0014_code-style-guide.md | 1 - vault/_lessons/0016_test-practices.md | 21 +++++++ vault/docs/_runs/202608101223-docs-refresh.md | 1 + 12 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 vault/_lessons/0016_test-practices.md diff --git a/README.md b/README.md index 7d6e03ca..1e10b73f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # booping -A self-learning, project-scoped sprint workflow for Claude Code. booping turns a feature idea into a durable, on-disk loop — **groom → develop → retro → learn** — that uses sub-agents to avoid context rot, with an optional second-model cross-review of every plan. Every artifact (plans, retros, lessons, sprint snapshots) lives under `~/Claude/{project}/`, one folder per codebase, so weeks-long programs stay legible long after the session ends. +A self-learning, project-scoped sprint workflow for Claude Code. booping turns a feature idea into a durable, on-disk loop — **groom → develop → retro → learn** — that uses sub-agents to avoid context rot, with an optional second-model cross-review of every plan. Every artifact (plans, retros, lessons, sprint snapshots) lives under `~/Claude/{project}/`, one folder per codebase. ![Vault open in Obsidian](docs/images/vault-in-obsidian.webp) @@ -8,15 +8,15 @@ A self-learning, project-scoped sprint workflow for Claude Code. booping turns a ## What it's for -The core use case: take a feature at whatever maturity it arrives — a rough idea over an existing codebase, a half-formed brief, a detailed PRD — and drive it through one loop: groom it into a plan, develop it, review the diff, retro what shipped. +Take a feature at whatever maturity it arrives — a rough idea over an existing codebase, a half-formed brief, a detailed PRD — and drive it through one loop: groom it into a plan, develop it, review the diff, retro what shipped. -booping is **not** spec-driven development — it doesn't keep specs as a living mirror of the codebase. It's closer to scrum sprints: a plan is a sprint artifact, done and forgotten once it ships; what persists is the vault's history and the lessons the loop distils from it. The core is flexible, though — playbooks are plain markdown procedures, and when the shipped loop doesn't fit, you write your own (see [Extensibility](#extensibility)). +booping is **not** spec-driven development — it doesn't keep specs as a living mirror of the codebase. It's closer to scrum sprints: a plan is a sprint artifact, done and forgotten once it ships; what persists is the vault's history and the lessons the loop distils from it. Playbooks are plain markdown procedures — when the shipped loop doesn't fit, you write your own (see [Extensibility](#extensibility)). ## Obsidian-ready by design -The vault at `~/Claude/{project}/` is markdown-only with YAML frontmatter that Obsidian renders natively as Properties. One vault per project, side-by-side with whatever else you keep in `~/Claude/`. No proprietary database, no lock-in — just files you can grep, version, and edit by hand. +The vault at `~/Claude/{project}/` is markdown-only with YAML frontmatter that Obsidian renders natively as Properties. One vault per project, side-by-side with whatever else you keep in `~/Claude/`. No lock-in — just files you can grep, version, and edit by hand. -This repo is developed with booping itself, and its vault is checked in at [`vault/`](vault/) — browse a [finished plan](vault/plans/202608081300_session-time-metrics/index.md), the [targeted lessons](vault/_lessons/) the loop has accumulated, or a [retrospective](vault/retrospectives/20260722-seven-plan-retro.md) to see what the artifacts look like in practice. +This repo is developed with booping itself, and its vault is checked in at [`vault/`](vault/) — browse a [finished plan](vault/plans/202608081300_session-time-metrics/index.md), the [targeted lessons](vault/_lessons/) the loop has accumulated, or a [retrospective](vault/retrospectives/20260722-seven-plan-retro.md). ## Disclaimer @@ -24,7 +24,7 @@ booping is aimed at **experienced developers and tech leads** — people comfort It's built for **iterative, agile-style development**: maintenance, incremental features, or growing a project sprint by sprint. It is **not** a waterfall tool — don't hand it a whole-project spec and expect a finished product. One plan is one sprint; the loop compounds across many. -Per-project configuration tunes the framework to each codebase: drop a `config.yaml` into your vault and it overrides the defaults — sprint scale, task types, branch conventions and agent wiring are the natural targets. The `code-review` playbook is a side-route for stack-aware review of a finished plan's diff, recorded as its own artifact under `codereviews/`. +Drop a `config.yaml` into your vault and it overrides the defaults — sprint scale, task types, branch conventions and agent wiring are the natural targets. The `code-review` playbook (`/playbook code-review`) is a side-route for stack-aware review of a finished plan's diff, recorded as its own artifact under `codereviews/`. **A note on maturity:** v1.0 works — the whole loop is dogfooded on this repo — but it's beta-grade. Expect rough edges (eval suites pending review, some skills-era legacy in the core). Installation trouble? Run `/playbook setup` and discuss the issue with it. @@ -52,13 +52,13 @@ Inside Claude Code, register the marketplace once, then install the plugin: Update later via `/plugin update booping` (or from the `/plugin` UI). -After installing, `cd` into the target repo and run `/playbook setup`. It settles the machine config, then scaffolds the vault (`plans/`, `retrospectives/`, `codereviews/`, `_lessons/`, `notes/`) and writes the `.booping` marker. Anything already in place is detected and skipped. +After installing, `cd` into the target repo and run `/playbook setup`. It settles the machine config, then scaffolds the vault (`plans/`, `retrospectives/`, `codereviews/`, `_lessons/`, `notes/`) and writes the `.booping` marker. Anything already in place is detected and skipped. The `booping` CLI logs its invocations to `.booping.log` at the vault root. ## Quick start For a hand-holding walkthrough and per-command reference, see the [docs site](https://A.github.io/claude-booping/). -The full loop is five steps, each a **playbook** driven by `/playbook` — the plugin's single entry point. Run them in order: +The full loop is five steps, each a **playbook** driven by `/playbook` — the plugin's one shipped skill and single entry point. Run them in order: ```bash # Inside the target repo, once: @@ -89,23 +89,23 @@ Candidates are listed for you if you forget the exact path. There is no shared status table — **each playbook has its own status vocabulary** and advances its own run artifact through it. Groom and develop run on the plan, so its `status:` frontmatter is whatever those two last wrote, and the plan track ends when develop closes it — at `done`, or at `fail` or `cancelled`. Retro and learn are a **separate track** over a standalone retrospective under `retrospectives/`; code review is a third, over one file per run under `codereviews/`. The statuses those tracks write are their own artifact's, never the plan's. ```text -groom framing → researching → drafting → cross-reviewing → presenting - → awaiting-approval → ready-for-dev (terminal) - (loopbacks: drafting → researching, awaiting-approval → drafting) - (any non-terminal status → cancelled (terminal)) +groom framing → researching → drafting → cross-reviewing → presenting + → awaiting-approval → ready-for-dev (terminal) + (loopbacks: drafting → researching, awaiting-approval → drafting) + (any non-terminal status → cancelled (terminal)) -develop awaiting-approval → ready-for-dev → in-progress - → done (terminal) | fail (terminal) - (any non-terminal status → cancelled (terminal)) +develop awaiting-approval → ready-for-dev → in-progress + → done (terminal) | fail (terminal) + (any non-terminal status → cancelled (terminal)) -retro awaiting-retro → awaiting-learning (terminal) [on retrospectives/{slug}.md] +retro awaiting-retro → awaiting-learning (terminal) [on retrospectives/{slug}.md] -learn awaiting-learning → done (terminal) [on retrospectives/{slug}.md] +learn awaiting-learning → done (terminal) [on retrospectives/{slug}.md] -review in-agent-review → human-review → done (terminal) [on codereviews/{dir}/{ts}.md] +code-review in-agent-review → human-review → done (terminal) [on codereviews/{dir}/{ts}.md] ``` -The tracks join the plan by frontmatter, not by status. A finished plan carries `retro: null` until a retrospective covers it, at which point retro stamps the retrospective's path there (or `skipped` when you skip it) — that null is the retro queue. Review joins by a **list**: `code_reviews:` starts null and every closing review appends its path, so the key reads as history rather than a queue flag, and the review queue is every `done` plan — a second or third pass over the same plan is ordinary, not an exception. +The tracks join the plan by frontmatter, not by status. A finished plan carries `retro: null` until a retrospective covers it, at which point retro stamps the retrospective's path there (or `skipped` when you skip it) — that null is the retro queue. Review joins by a **list**: `code_reviews:` starts empty and every closing review appends its path, so the key reads as history rather than a queue flag, and the review queue is every `done` plan, re-review included. ## Statuses @@ -130,7 +130,7 @@ A plan's frontmatter status is written by groom or develop. Retro and learn writ **groom / develop** -- **`cancelled`** *(terminal in both machines)* — you called the run off. Reachable from every non-terminal status of either machine, so a plan can be abandoned at any point without inventing a fake outcome. Groom snapshots the plan into the vault on the way out; develop stamps `completed:`. +- **`cancelled`** *(terminal in both machines)* — you called the run off. Reachable from every non-terminal status of either machine. Groom snapshots the plan into the vault on the way out; develop stamps `completed:`. **retro / learn** — on the retrospective at `retrospectives/{slug}.md`, not on a plan. @@ -150,7 +150,7 @@ This README narrates the statuses — each playbook is the authority on its own ## Sprints & SPs -In booping, a **plan is a sprint** — the unit groom produces and develop executes end-to-end. Story Points (SP) measure the sprint's **complexity and review burden**, not effort or time. A 20-SP sprint might take a couple of hours on one project and a full day on another; SPs give you a feel for its size and review weight, independent of how fast the underlying work happens. +In booping, a **plan is a sprint** — the unit groom produces and develop executes end-to-end. Story Points (SP) measure the sprint's **complexity and review burden**, not effort or time. A 20-SP sprint might take a couple of hours on one project and a full day on another. The 1–5 scale: @@ -160,7 +160,7 @@ The 1–5 scale: - **4 SP** — Complex task, medium risk, may need small research but clear enough. - **5 SP** — Research task — developer needs to clarify and decompose further before proceeding. -Sprints over **35 SP** get hard to keep reviewable, so groom suggests a split into sibling sprints above that mark. It's a soft cap, **not a velocity** — booping has no fixed cadence and no per-week capacity. Tasks at 5 SP must be re-decomposed; tasks at 1 SP should be grouped into a single agent briefing. +Sprints over **35 SP** get hard to keep reviewable, so groom suggests a split into sibling sprints above that mark. It's a soft cap, **not a velocity** — booping has no fixed cadence and no per-week capacity. Tasks at 5 SP must be re-decomposed; develop bundles up to two consecutive milestones into a single agent briefing, so milestones are sized against that combined review burden. ## Extensibility @@ -173,8 +173,6 @@ Playbooks stay wide-domain and stack-agnostic. Project-specific concerns live en ## Learning -Retro and learn are the loop that makes booping worth more than the sum of its sprints. - `retro` reads the working set of finished plans, mines the session logs and git diff for what actually shipped, takes your raw feedback first, and writes one standalone `~/Claude/{project}/retrospectives/{slug}.md` — what worked, what didn't, divergences from spec, a goal verdict per plan. One retrospective can cover several plans; each gets its `retro:` stamped with the file's path. `learn` then reviews the retrospective with you, picks the durable findings, and routes each to one of two destinations: a targeted lesson (`~/Claude/{project}/_lessons/{N}_{title}.md`, carrying a `targets:` list), or a one-line bullet in the attached repo's `CLAUDE.md`. Lessons are injected into the playbooks, steps, agents and skills they name. You confirm the whole review table before anything lands. diff --git a/documentation/code_review.md b/documentation/code_review.md index 10097d7d..54f1e485 100644 --- a/documentation/code_review.md +++ b/documentation/code_review.md @@ -1,12 +1,12 @@ # code-review playbook -Code review is a **playbook**, not a skill: +Code review is a playbook driven by `/playbook`, the plugin's single skill and the only way to start a review: ```text /playbook code-review ``` -A run reviews one confirmed scope end to end — `scope` settles what to look at (any `done` plan, the latest commits, or a named target) and opens the run's artifact, `review` returns severity-classified findings from a detached pass, `present` writes them to the artifact and collects your verdict, `resolve` acts on that verdict and closes the run. +A run reviews one confirmed scope end to end: `scope` settles what to look at (any `done` plan, the latest commits, or a named target) and opens the artifact, `review` returns severity-classified findings from a detached pass, `present` writes them and collects your verdict, `resolve` acts on it and closes the run. ## The review artifact @@ -16,15 +16,15 @@ The artifact is the run's own state: a stopped review is **resumable**, and a se ## Statuses -Declared in `playbooks/code-review/playbook.yaml`'s `states:` block; the status lives in the artifact, never in the reviewed plan: +The status lives in the artifact, never in the reviewed plan: - **`in-agent-review`** — the artifact is open and the detached pass is producing findings. - **`human-review`** — the findings are on record, your verdict is pending. - **`done`** — every finding is resolved (applied, delegated or dropped) and recorded. -The run works from the vault root and addresses the review file explicitly, so a stopped run resumes with `booping playbook-state code-review --workdir {vault} --target codereviews/{dir}/{ts}.md`. +The run works from the vault root and addresses the review file explicitly; a stopped run resumes with `booping playbook-state code-review --workdir {vault} --target codereviews/{dir}/{ts}.md`. -Closing the run stamps `reviewed_at` on the artifact and runs the `close-code-review` hook, which appends the artifact's path to the reviewed plan's `code_reviews:` list and commits the vault. An ad-hoc review (`plan: null`) skips the append and only commits. The plan's `status:` is never touched. +Closing the run stamps `reviewed_at` on the artifact, appends the artifact's path to the reviewed plan's `code_reviews:` list, and commits the vault. An ad-hoc review (`plan: null`) skips the append and only commits. The plan's `status:` is never touched. ## The queue @@ -32,6 +32,6 @@ Closing the run stamps `reviewed_at` on the artifact and runs the `close-code-re ## Review templates -Review checklists are markdown files in a `review_templates/` directory, layered core → global → project: the plugin ships a core set, your global home directory adds machine-wide templates, your vault's `review_templates/` holds the project's own. A later level overrides an earlier one by name. See [Vault → `review_templates/`](vault.md#review_templates) for the project-level directory. +Review checklists are markdown files in `review_templates/`, layered core → global → project: the plugin ships a core set, your global home directory adds machine-wide ones, your vault's `review_templates/` holds the project's own. A later level overrides an earlier one by name. See [Vault → `review_templates/`](vault.md#review_templates) for the project-level directory. See [Playbooks → Shipped playbooks](playbook.md#shipped-playbooks) for how playbooks are driven. diff --git a/documentation/develop.md b/documentation/develop.md index 2c6cbf4f..41b24094 100644 --- a/documentation/develop.md +++ b/documentation/develop.md @@ -10,7 +10,7 @@ Development is a **playbook**, not a skill — driven by [`/playbook`](playbook. ## What it does -The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**: the plan track ends there, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own, declared in `playbooks/develop/playbook.yaml`'s `states:` block, not in a shared lifecycle. Run state lives in the plan's own `index.md`, so a stopped sprint is **resumable**. +The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own — every playbook declares its own. Run state lives in the plan's own `index.md`, so a stopped sprint is **resumable**. Five steps, in dependency order: @@ -24,16 +24,16 @@ Five steps, in dependency order: The runner edits no application code — all coding is delegated; it owns reads/writes against the vault, briefing assembly, verification, and commits. -Every milestone group runs in **one session** by default. Stopping after each milestone is a request you make at invocation time. +Every milestone group runs in **one session**. ## Starting a run ```text /playbook develop -/playbook develop — plans/202604301120_src-files-build-pipeline/index.md, pause after each milestone +/playbook develop — plans/202604301120_src-files-build-pipeline/index.md ``` -Bare invocation resolves the plan from what the session just groomed or from the vault's queue. Name a plan path to target a specific one. Free-text reaches `intake` verbatim — useful for stop-after-each-milestone or model-comparison runs. +Bare invocation lists the vault's plans at `ready-for-dev` or `awaiting-approval` and asks you to pick one. Name a plan path to target a specific one. To resume a sprint already `in-progress`, invoke the playbook against the same plan: `playbook-state` reports the frontier and the run picks up at the first milestone whose DoDs are not all `[x]`. @@ -43,6 +43,12 @@ To resume a sprint already `in-progress`, invoke the playbook against the same p That confirmation is the playbook's single review gate. +## Session metrics + +[groom](groom.md) and develop transitions record the plan's Claude Code session ids as they go; when develop closes the plan, `booping session-stats` mines those session logs and stamps the plan's frontmatter with `metrics_*` keys: active work minutes, the models that ran the sessions, and token counts (input, output, cache creation, cache read) totalled over the plan's sessions — the command's own output breaks the same numbers down per session. Active time is idle-aware — minutes spent waiting on you, a question unanswered or a tool call rejected, are excluded. + +`booping session-stats` is the one metrics surface. The stamped numbers roll up into the vault's `sprints.md` columns, so a sprint's cost is visible without opening each plan — see [Vault](vault.md). + ## Best practices ### Run code review in a fresh session @@ -51,11 +57,11 @@ Run a [code-review](code_review.md) pass from a **fresh session**, not the one t ### Try alternative models for implementation -Plans are reproducible files, so the same plan runs with different models — a cheaper or faster model for the implementation pass, Opus for [code-review](code_review.md) afterward. The plan is the controlled input, the diff the controlled output: swap models, compare diffs, calibrate which model gives acceptable quality on your codebase. +Plans are reproducible files, so the same plan runs with different models — a cheaper or faster model for the implementation pass, Opus for [code-review](code_review.md) afterward. Swap models, compare diffs, calibrate which model gives acceptable quality on your codebase. ### Review the code-review feedback list -The [code-review playbook](code_review.md) puts its findings in front of you and asks for a verdict on each one, then applies what you approved. Rule on the list explicitly: +The [code-review playbook](code_review.md) puts its findings in front of you and asks for a verdict on each one, then applies what you approved. Rule explicitly: - **BLOCKER** — must be applied before merge. Trivial fixes happen inline in the review session; non-trivial fixes go back through `booping-developer` via a follow-up briefing. - **SUGGESTION** — apply if the cost is low; drop it explicitly otherwise. Do not silently drop. @@ -67,7 +73,7 @@ The discipline is the [groom playbook](groom.md)'s for cross-review findings: ev The playbook reads these keys from `src/config.yaml`. See [Project config](project_config.md) for the deep-merge override mechanics; per-project tweaks live in `~/Claude/{project}/config.yaml`. -- **`core.sprint.max_milestones_per_agent`** — maximum consecutive milestones grouped into a single `booping-developer` briefing. Grouping happens only when the milestones share enough context that one agent handling them in sequence is cheaper than a fresh agent per milestone. Default `2`. +- **`core.sprint.max_milestones_per_agent`** — maximum consecutive milestones grouped into a single `booping-developer` briefing; grouping happens only when the milestones share enough context that one agent in sequence beats a fresh agent per milestone. Default `2`. - **`core.develop_playbook.git.branches`** — list of `{branch, when}` entries that map plan `type` (or freeform descriptors) to a branch prefix; `provision` picks from this list. - **`core.develop_playbook.git.commit_message`** — message format string used for in-sprint commits. Override per-project to enforce a different commit shape. - **`core.develop_playbook.agents`** — the agents the playbook may delegate to, with `good_for` / `bad_for` guidance. `booping-developer` is the implementation channel; `booping-researcher` is reserved for the intake drift spot-check across many plan-named files. diff --git a/documentation/groom.md b/documentation/groom.md index 958f832f..cdc082aa 100644 --- a/documentation/groom.md +++ b/documentation/groom.md @@ -12,24 +12,24 @@ Grooming is a **playbook**, not a skill — it is driven by [`/playbook`](playbo Run states: `framing` → `researching` → `drafting` → `cross-reviewing` → `presenting` → `awaiting-approval` → `ready-for-dev`. Two loopbacks: `drafting → researching` when the design needs blast radius the research pass missed, and `awaiting-approval → drafting` when your change request touches the plan itself. -Almost every step runs in your session — inline, or assisted, meaning heavy reads go to the research agent, which returns a bounded summary. The one exception is `cross-review`, which hands the drafted plan to a second-model reviewer and does nothing unless you name one. `present`, near the end of the run, is the run's single approval gate. +Almost every step runs in your session — inline, or assisted (heavy reads go to the research agent, which returns a bounded summary). The exception is `cross-review`, which hands the drafted plan to a second-model reviewer and does nothing unless you name one. `present` is the run's single approval gate. -The output is a plan directory `~/Claude/{project}/plans/{slug}/` whose `index.md` carries the plan and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads. The intake briefing and any web-research notes land beside it, so everything the run gathered stays with the plan. The directory also doubles as the run workdir, which makes a groom run **resumable**: its run state lives in the same `index.md`. +The output is a plan directory `~/Claude/{project}/plans/{slug}/` whose `index.md` carries the plan and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads. Groom creates the directory with one `booping scaffold` call from a config-declared tree, so the fresh `index.md` arrives complete — including a real `commit:` stamped with repo HEAD at creation. The intake briefing and any web-research notes land beside it. The directory doubles as the run workdir, so a groom run is **resumable**: its run state lives in the same `index.md`. -A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else. A vault still holding flat `plans/{slug}.md` files from an older release converts them by running `/playbook migrate`. +A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else. A vault still holding flat `plans/{slug}.md` files converts them with `/playbook migrate`. Six steps, in dependency order: | Step | What it does | |------|--------------| -| `intake` | Clarify the request, settle scope, create the plan directory with its briefing and identity frontmatter | +| `intake` | Clarify the request, settle scope, scaffold the plan directory — briefing, identity frontmatter, `commit:` at repo HEAD — with one `booping scaffold` call | | `research-codebase` | Map the blast radius — files, modules, integrations, prior art (assisted: heavy reads go to the research agent) | | `research-web` | Check external practice where the design is uncertain, and verify package versions, image tags, API endpoints and CLI flags against current docs | | `draft-plan` | Design with you in conversation, then write the plan body against a plan template | | `cross-review` | Hand the drafted plan to a second-model reviewer for severity findings — skipped unless `core.groom_playbook.cross_review_agent` names one (unset by default) | | `present` | Present approach, milestones and SP totals; the run's single approval gate | -Design work happens in conversation inside `draft-plan` — refinement and decomposition are part of drafting, not separate steps. +Refinement and decomposition happen inside `draft-plan`. ## Starting a run @@ -70,7 +70,6 @@ existing one in apps/api/middleware/. Useful things to mention up front: -- **Stop after each milestone** — tell the develop playbook (later) to pause between milestones for review; groom records this as a plan note. - **Reference a template** — "use the bug-investigation template" selects a specific plan template. Branch selection is **not** groom's job — the [develop playbook](develop.md)'s `provision` step picks and confirms it. @@ -83,14 +82,12 @@ See [Vault](vault.md) for the directory layout. ## Reviewing the plan -`present` is the run's **only** review gate — the plan is not ready for development until you approve it. - -What to check before approving: +What to check before approving at `present`, the run's **only** review gate: - **Goal is sharp.** The `summary` in frontmatter matches the request, with no scope creep. - **Milestones cover the goal end-to-end.** No silent gaps, no "and then ..." vagueness in the last milestone. - **Tasks are sized honestly.** No 5-SP tasks except deliberate research spikes (see Story points). -- **Definitions of done are verifiable.** Each task DoD checkbox is something you can mechanically confirm — not "code looks good". +- **Definitions of done are verifiable.** Each DoD checkbox is mechanically confirmable — not "code looks good". Approve explicitly ("looks good", "ship it") to move the plan to `ready-for-dev`, groom's terminal status and the queue the [develop playbook](develop.md) claims from. A change request touching architecture, scope, milestones, tasks or estimates sends the run back to `drafting`. diff --git a/documentation/index.md b/documentation/index.md index df43155f..e5045fe9 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -1,8 +1,8 @@ # booping -A self-learning, project-scoped sprint workflow for Claude Code. booping turns a feature idea into a durable, on-disk loop — **groom → develop → retro → learn** — that spreads work across sub-agents to avoid context rot, with an optional second-model cross-review of every plan. Every artifact — plans, retros, code reviews, lessons — lives in the per-project vault (`~/Claude/{project}/` by default, or a repo-local directory), one folder per codebase, so weeks-long programs stay legible. +A self-learning, project-scoped sprint workflow for Claude Code. booping turns a feature idea into a durable, on-disk loop — **groom → develop → retro → learn** — spread across sub-agents to avoid context rot, with an optional second-model cross-review of every plan. Plans, retros, code reviews and lessons live in the per-project vault (`~/Claude/{project}/` by default, or a repo-local directory), one folder per codebase, so weeks-long programs stay legible. -The vault is plain markdown with YAML frontmatter, so Obsidian renders it natively as Properties. No proprietary database, no lock-in — just files you can grep, version, and edit by hand. +The vault is plain markdown with YAML frontmatter, so Obsidian renders it natively as Properties. No database, no lock-in — files you can grep, version, and edit by hand. ## The loop @@ -28,14 +28,14 @@ The vault is plain markdown with YAML frontmatter, so Obsidian renders it native └────────┘ ``` -Every plan walks this loop once; the next inherits the lessons. Setup, grooming, development, code review, retro and learn are all **playbooks**, driven by `/playbook` — the plugin's only shipped skill. develop closes the plan at `done`; retro + learn and code-review then run as separate tracks, each advancing its own standalone artifact — `retrospectives/{slug}.md` and per-review files under `codereviews/` — while the plan stays `done`. +Every plan walks this loop once; the next inherits the lessons. Setup, grooming, development, code review, retro and learn are all **playbooks**, driven by `/playbook` — the plugin's only shipped skill. develop closes the plan at `done`; retro + learn and code-review then run as separate tracks over their own standalone artifacts — `retrospectives/{slug}.md` and per-review files under `codereviews/` — while the plan stays `done`. ## Why booping -- **Artifacts as a meta-source.** Plans, retros, and lessons are durable on-disk markdown you can mine for documentation, onboarding material, research notes, or a historical trace of how a codebase actually evolved — the vault is its own knowledge base. -- **A benchmarking surface.** Plans are reproducible files: run the same one twice with different models (Opus vs. Sonnet vs. GLM) and compare the diffs to see how each behaves on your codebase — the controlled inputs an honest comparison needs. -- **Agile-iteration scaffolding made durable.** Sprint cadence, reviews, retrospectives, and lessons are social rituals that decay when nobody writes them down. booping gives each a concrete file and a concrete command, so the loop survives sessions, machines, and weeks of context loss. -- **Story points as a personal estimation track record.** Every plan carries an SP estimate; every retro records what actually shipped. Over many sprints that accumulates into honest data on how your estimates compare to reality on this specific codebase — a private calibration log no ticket tracker gives you. +- **Artifacts as a meta-source.** Plans, retros and lessons are durable on-disk markdown you can mine for documentation, onboarding material, research notes, or a historical trace of how a codebase actually evolved. +- **A benchmarking surface.** Plans are reproducible files: run the same one with different models (Opus vs. Sonnet vs. GLM) and compare the diffs — the controlled inputs an honest comparison needs. +- **Agile-iteration scaffolding made durable.** Sprint cadence, reviews, retrospectives and lessons are social rituals that decay unwritten; booping gives each a concrete file and a concrete command, so the loop survives sessions, machines and weeks of context loss. +- **Story points as a personal estimation track record.** Every plan carries an SP estimate; every retro records what actually shipped. Over many sprints that accumulates into honest data on your estimates versus reality on this specific codebase — a private calibration log no ticket tracker gives you. ## Read next @@ -44,8 +44,8 @@ Every plan walks this loop once; the next inherits the lessons. Setup, grooming, - [Vault](vault.md) — full tour of `~/Claude/{project}/`: what every file and directory is for. - [groom playbook](groom.md) — spec a sprint, with cross-review and the user-approval gate. - [develop playbook](develop.md) — claim a ready plan and execute milestones. -- [code-review playbook](code_review.md) — stack-aware review of a confirmed scope, recorded under `codereviews/`. +- [code-review playbook](code_review.md) — `/playbook code-review`: stack-aware review of a confirmed scope, recorded under `codereviews/`. - [retro playbook](retro.md) — capture what actually shipped vs. the spec. - [learn playbook](learn.md) — fold retro findings into durable rules. -- [Playbooks](playbook.md) — multi-step procedures driven by `/playbook`. *Unstable — work in progress.* +- [Playbooks](playbook.md) — multi-step procedures driven by `/playbook`, the framework at the plugin's core. - [Project config](project_config.md) — the `config.yaml` you can drop in your vault, and how it overrides booping's defaults. diff --git a/documentation/playbook.md b/documentation/playbook.md index f9d8f584..49988e23 100644 --- a/documentation/playbook.md +++ b/documentation/playbook.md @@ -1,11 +1,8 @@ # Playbooks -!!! warning "Unstable — work in progress" - Playbooks are an experimental feature. The manifest format, step frontmatter, and `/playbook` behaviour may change in breaking ways between releases. +A **playbook** is a multi-step guided procedure driven by the `/playbook` skill — the one skill booping ships. Where a skill is a fixed workflow, a playbook is yours to write: prompt steps, each optionally detached into a sub-agent, with review gates that pause for inspection. Most live in your Project Vault; a few ship with the plugin (see [Levels](#levels)). -A **playbook** is a multi-step guided procedure driven by the `/playbook` skill — the one skill booping ships. Where a skill is a fixed workflow, a playbook is yours to write: prompt steps, each optionally detached into a sub-agent, with review gates that pause for your inspection. Most playbooks are yours and live in your Project Vault; a few ship with the plugin (see [Levels](#levels)). - -A playbook's **structure** lives in `playbook.yaml`: the `graph:` (which steps run, in what order, which in parallel) and the optional `states:` (named state machines that persist run state on disk so a run can be resumed). `playbook.md` keeps **identity and prose** — the manifest frontmatter (`name`, `title`, `summary`, `trigger`, …) and the preamble body. Bodies are plain markdown by default; a playbook can opt into [Jinja rendering](#jinja-bodies) for live project data. Author a playbook by hand and it shows up in `/playbook` immediately. +A playbook's **structure** lives in `playbook.yaml`: the `graph:` (which steps run, in what order, which in parallel) and the optional `states:` (named state machines that persist run state on disk so a run can be resumed). `playbook.md` keeps **identity and prose** — the manifest frontmatter (`name`, `title`, `summary`, `trigger`, …) and the preamble body. Bodies are plain markdown by default; a playbook can opt into [Jinja rendering](#jinja-bodies) for live project data. Author one by hand and it shows up in `/playbook` immediately. !!! note "Legacy: `graph:` in `playbook.md` frontmatter" A playbook with no `playbook.yaml` still works: `graph:` is read from `playbook.md` frontmatter. Declaring `graph:` in **both** places is a blocking STOP — keep exactly one. `state:` / `states:` are `playbook.yaml`-only; there is no frontmatter fallback. @@ -18,11 +15,11 @@ Playbooks are discovered from the core, global and project levels: - **Global** — `<home_dir>/_playbooks/<name>/` (default `~/Claude/_playbooks/`). Shared across every project on the machine. - **Project** — `{vault}/_playbooks/<name>/`. Specific to one Project Vault. -**A playbook `name` must be unique across the core, global and project levels.** The same name at two levels is a name clash: `/playbook` marks the entry `⚠ clash` in its listing, and rendering the playbook returns a blocking STOP notice instead of the procedure — neither copy runs until one of them is renamed. To adapt a playbook you didn't write, copy it under a new name or attach [lessons](#lessons) to it. Directories whose name starts with `_` (e.g. `_lib`, `_partials`) are skipped, so shared helper content can sit alongside playbooks. +**A playbook `name` must be unique across levels.** The same name at two levels is a name clash: `/playbook` marks the entry `⚠ clash` in its listing, and rendering the playbook returns a blocking STOP notice instead of the procedure — neither copy runs until one of them is renamed. To adapt a playbook you didn't write, copy it under a new name or attach [lessons](#lessons) to it. Directories whose name starts with `_` (e.g. `_lib`, `_partials`) are skipped, so shared helper content can sit alongside playbooks. ### Shipped playbooks -Core playbooks ship with the plugin and own the main workflow: +Core playbooks own the main workflow: - **`setup`** — machine config, then vault scaffold and `.booping` marker. See [Install](install.md). - **`groom`** — spec a sprint (intake → codebase and web research → draft → cross-review → present). See [groom](groom.md). @@ -33,9 +30,7 @@ Core playbooks ship with the plugin and own the main workflow: - **`migrate`** — bring a vault up to the plugin's current migration watermark. - **`playbook-authoring`** — the procedure for writing a new playbook. -Run any of them with `/playbook <name>`. - -A playbook of your own sharing a core name clashes with it rather than replaces it. To bend a shipped procedure to a project, add [lessons](#lessons) in `{vault}/_lessons/` targeting it; to fork it, copy the directory under a different name. +Run any of them with `/playbook <name>`. A playbook of your own sharing a core name clashes with it rather than replaces it. ## Layout @@ -51,7 +46,7 @@ Each playbook is a directory: _references/ # `_`-prefixed dirs are not steps — free workspace ``` -**A step is a directory and `prompt.md` is the step.** Every non-`_` subdirectory holding a `prompt.md` is a step, and its directory name is the step name the graph references. Nothing else in the directory is loaded — sibling files (fixtures, test configs, prompt variants like `base.md`) are invisible to the runner. A non-`_` subdirectory without a `prompt.md` is skipped with a warning. +**Every non-`_` subdirectory holding a `prompt.md` is a step**, and its directory name is the step name the graph references. Nothing else in the directory is loaded — sibling files (fixtures, test configs, prompt variants like `base.md`) are invisible to the runner. A non-`_` subdirectory without a `prompt.md` is skipped with a warning. Directory order on disk is irrelevant — **the `graph:` decides which steps run and in what order**. `_`-prefixed directories (`_references/`, `_fixtures/`) are never steps, so disabled steps and shared material can sit alongside without being wired in. @@ -94,22 +89,20 @@ Exact names only — no globs, no wildcards, no negation: A lesson may carry several entries, of mixed forms. An entry matching none of these forms is ignored (see [Notices](#notices)). -**Playbook authors do nothing.** Injection happens in `booping render-playbook` itself — there is no lessons partial to include, and a playbook cannot opt out. +Injection happens in `booping render-playbook` itself — there is no lessons partial to include, and a playbook cannot opt out. !!! warning "Agent targets reach booping's own agents only" `agent:` targets are injected into the plugin's internal agents — `agent:booping-developer` and `agent:booping-researcher`. An [external or global agent](integrating-external-agents.md) at `~/.claude/agents/<id>.md`, and a sub-agent spawned by model tier (`detached: sonnet:high`), receive **no** targeted lessons: their bodies are not rendered by booping. To reach one of those, target the step it performs (`{playbook}/{step}`) — the step prompt is fetched by the agent itself. **Same filename in both directories → the project copy wins.** A `0002_receipts.md` present in `~/Claude/_lessons/` and in `{vault}/_lessons/` loads once, from the vault. Give lessons distinct names unless you mean to shadow one. -**Where they surface.** Playbook-targeted lessons render as a `## Lessons` section in the composed procedure, between the preamble and `## Playbook Steps`; they bind the driver for the whole run. Step-targeted lessons are appended to that step's `booping render-playbook <name> --step <step>` output, so they reach exactly the agent running that step (and the embedded body when the playbook renders steps inline). `--no-lessons` suppresses both sections *and* the lesson notices, so the output matches a lesson-free vault byte for byte. +**Where they surface.** Playbook-targeted lessons render as a `## Lessons` section in the composed procedure, between the preamble and `## Playbook Steps`; they bind the driver for the whole run. Step-targeted lessons are appended to that step's `booping render-playbook <name> --step <step>` output, so they reach exactly the agent running that step (and the embedded body when the playbook renders steps inline). `agent:{id}` and `skill:{name}` lessons render inside the named agent's or skill's body when it loads — `skill:playbook` shapes the `/playbook` skill itself. `--no-lessons` suppresses both sections *and* the lesson notices, so the output matches a lesson-free vault byte for byte. !!! note "Retired: playbook-local `_lessons/` and `step:`" A `_lessons/` directory inside a discovery root or inside a playbook directory is not read, and neither is a lesson's `step:` frontmatter key. Move those files into `{vault}/_lessons/` (or `<home_dir>/_lessons/`) and express `step: draft` as `targets: [<playbook>/draft]`. While a retired directory still holds markdown, every render of that playbook emits a non-blocking migration note naming it. ## The manifest -Structure lives in `playbook.yaml`, identity and prose in `playbook.md`. - ### `playbook.yaml` - `graph` — mapping of **node name → node**. A node whose value is a **list** is a plain step and the list is its dependency step names; a node whose value is a **mapping** is a [subgraph](#subgraphs). This is the whole structure: membership (only mapped steps run), order (a step runs after all its dependencies), and parallelism (steps whose dependencies are all satisfied by earlier waves run together). @@ -153,10 +146,10 @@ review_gate: Plan draft ready — approve before presenting? A step runs at one of three levels. Only the third has mechanics; the first two are the same runtime shape and differ in what the step body asks for. - **inline** — the runner fetches the step body and performs the step itself, in the driving conversation. Everything the step reads lands in the driver's context. No frontmatter key. -- **assisted** — the runner still performs the step, but delegates the heavy reads or research inside it to the configured researcher agent, which returns a compressed summary. The driver's context holds the summary, not the sources. Expressed as prose in the step body — no frontmatter key. The spawn's agent id comes back with the summary, and a follow-up question is sent to that same agent by id — its context, the sources already read, intact — rather than a fresh spawn re-reading everything. +- **assisted** — the runner still performs the step, but delegates the heavy reads or research inside it to the configured researcher agent, which returns a compressed summary. The driver's context holds the summary, not the sources. Expressed as prose in the step body — no frontmatter key. The spawn's agent id comes back with the summary, so a follow-up goes to that same agent by id — its context, the sources already read, intact — rather than a fresh spawn re-reading everything. - **detached** — an agent fetches and performs the whole step body; the runner sees only the returned receipt. This is the only level with mechanics: the `detached:` frontmatter key. -The researcher an assisted step delegates to is the `core.research_agent` config key (core default `booping:booping-researcher`), overridable per project like any other config value. A `jinja: true` body reads it as `{{ config.core.research_agent }}` — the same way an optional key is read defensively with `{% if config.core.get("my_key") %}`. +The researcher an assisted step delegates to is the `core.research_agent` config key (core default `booping:booping-researcher`), overridable per project. A `jinja: true` body reads it as `{{ config.core.research_agent }}`; an optional key is read defensively with `{% if config.core.get("my_key") %}`. ### The `detached` grammar @@ -175,7 +168,7 @@ detached: "{{ config.core.groom_playbook.cross_review_agent or '' }}" --- ``` -When the key it names is absent from the merged config the value renders empty, and the step degrades to runner-performed rather than spawning an agent with no name — so a playbook can offer an optional reviewer and let the preamble say to skip the step when none is configured. +When the key it names is absent from the merged config the value renders empty and the step degrades to runner-performed rather than spawning an agent with no name — so a playbook can offer an optional reviewer, and let the preamble say to skip the step when none is configured. ## Jinja bodies @@ -200,21 +193,19 @@ requires_project: true {% include "_partials/_git_guide.j2" %} ``` -Two things change once you opt in: - - **Project context is required.** Rendering a `jinja: true` playbook without a project attached produces a blocking `STOP` notice instead of the procedure. Pair it with `requires_project: true`. - **A template error is a blocking notice**, not a crash — the failure is reported in-band and the playbook refuses to run. -A Jinja body is meaningless until rendered — the `booping render-playbook <name> --step <step>` fetch every step body goes through returns it already rendered (see below). +A Jinja body is meaningless until rendered — the `booping render-playbook <name> --step <step>` fetch every step body goes through returns it already rendered. ## How the graph renders -`/playbook` renders the graph as a **step table** — one row per step, in dependency order, carrying the step name, its dependencies, its summary and its review gate. A step runs once every step in its `Dependencies` cell is done; steps whose dependencies are all satisfied run together. +`/playbook` renders the graph as a **step table** — one row per step, in dependency order, carrying the step name, its dependencies, its summary and its review gate. -- **Step bodies are fetched, not embedded** — by default every section ends with the command that fetches the body, the same for plain and `jinja: true` playbooks. The procedure the driver holds is therefore proportional to the graph — step table plus per-step metadata, never the bodies — and each body enters exactly one context, through the same `--step` fetch, only when its step runs. A runner-performed step's section is a metadata block (summary, dependencies, review gate) closed by *Run `booping render-playbook <name> --step <step>` for content.* A **detached** step's section is instead one order to the driver — its summary as a paragraph, its review gate when it has one, then *Tell the `<agent>` agent to get its instructions by calling this command: `booping render-playbook <name> --step <step>`.* Its dependencies and wave order are omitted there; the step table already carries them. +- **Step bodies are fetched, not embedded** — by default every section ends with the command that fetches the body, the same for plain and `jinja: true` playbooks. The procedure the driver holds is therefore proportional to the graph — step table plus per-step metadata, never bodies — and each body enters exactly one context, only when its step runs. A runner-performed step's section is a metadata block (summary, dependencies, review gate) closed by *Run `booping render-playbook <name> --step <step>` for content.* A **detached** step's section is instead one order to the driver — its summary as a paragraph, its review gate when it has one, then *Tell the `<agent>` agent to get its instructions by calling this command: `booping render-playbook <name> --step <step>`.* Its dependencies and wave order are omitted there; the step table already carries them. - **`inline_steps` embeds the runner's bodies** — with `inline_steps: true` in the manifest (or `--inline-steps` on the command), a runner-performed step's section carries its rendered body, with that step's lessons already appended, in place of *both* the metadata block and the fetch command; its summary, dependencies and review gate stay in the step table. Detached steps keep fetch-form, so their agents still fetch their own body. A body that fails to render is a blocking `STOP` notice like any other. - **Delegated steps fetch their own body** — the driver never runs the fetch command for a sub-agent step. It spawns the agent with a bootstrap prompt: the fetch command ("treat its stdout as your full instruction"), a `## Run-time context` block (project, plus `workdir` / `instance` where they apply), a `## Inputs` block assembled from the run-time context and prior steps' receipts, and a uniform `## Return` contract (`artifacts written + outcome, ≤ 5 lines`) — a richer contract belongs in the step body. A step without `detached:` is the only case where the driver runs the fetch itself and executes the stdout. -- **Steps that can run together must be `detached:`** — a step the runner performs itself can't run in parallel, so a step whose dependencies are satisfied at the same time as another's must be `detached:`. +- **Steps that can run together must be `detached:`** — a step the runner performs itself can't run in parallel, so steps whose dependencies are satisfied at the same time must all be `detached:`. - **Review gates pause after the batch** — once every step running together finishes, each one's gate is presented (labeled by step) and the run waits for your confirmation before the next batch. ## Subgraphs @@ -332,12 +323,14 @@ states: ### Hooks -Two hook forms are available on a transition: +Two hook forms on a transition: -- `frontmatter-update [<file>] <key>=<val> ...` — set frontmatter keys on the artifact, or on `<file>` when a target is given. +- `frontmatter-update [<file>] <key>=<val> ...` — set frontmatter keys on the artifact, or on `<file>` when a target is given. The hook form takes `key=val` pairs only, no flags. - `script <name> [args...]` — run the executable named `<name>`, passing every token after it as argv. -A hook value is **Jinja-rendered with the `macro` global**, the same one rendered bodies call — a timestamp is `completed="{{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }}"`, with the format at the call site and no bespoke token vocabulary. Because the clock goes through the macro system, a `macro_stubs:` mapping in the merged config pins it — the same config fragment `--stub-macro` merges on a render — so a transition is reproducible the way a render is. The hook string is tokenised with `shlex`, so quote any value carrying spaces — as above. A value with no Jinja in it passes through untouched, and a macro or Jinja error aborts the transition (exit 2) with the offending value on stderr. +The `booping frontmatter-update` **command** carries a flag the hook form does not: `--append <key>=<val>` appends the value to the **list** under `<key>` instead of setting it — a missing key is created as a one-entry list, a value the list already holds is not added again, and a key that already holds a scalar is an error. A `script` hook shelling out to it is how a history key such as `sessions:` grows across transitions without YAML editing. + +A hook value is **Jinja-rendered with the `macro` global**, the same one rendered bodies call — a timestamp is `completed="{{ macro('core.macros.date', '+%Y-%m-%d %H:%M') }}"`, with the format at the call site. Because the clock goes through the macro system, a `macro_stubs:` mapping in the merged config pins it — the same config fragment `--stub-macro` merges on a render — so a transition is reproducible the way a render is. The hook string is tokenised with `shlex`, so quote any value carrying spaces. A value with no Jinja in it passes through untouched, and a macro or Jinja error aborts the transition (exit 2) with the offending value on stderr. The repo's HEAD sha is likewise a macro, `core.macros.git_commit`, which carries `cwd: repo` so it resolves against the repo directory rather than the process cwd (the run workdir during a transition). @@ -374,15 +367,15 @@ with `<run-slug>` = `{YYYYMMDD}-<kebab-topic>`. Every state command takes `--wor ### Resume -Everything needed to resume lives on disk. `/playbook` runs `booping playbook-state <name> --workdir <workdir>` on entry — first run and every resume — and restarts from the reported frontier: work already past its status is skipped, the first non-terminal status is re-entered, and `not-started` means bootstrap on the first transition. Nothing hand-edits an artifact's `status:` or a hook-written key; only `playbook-transition` mutates run state. +`/playbook` runs `booping playbook-state <name> --workdir <workdir>` on entry — first run and every resume — and restarts from the reported frontier: work already past its status is skipped, the first non-terminal status is re-entered, and `not-started` means bootstrap on the first transition. Nothing hand-edits an artifact's `status:` or a hook-written key; only `playbook-transition` mutates run state. ### Cancellation -A run does not have to reach its success terminal. Declare a `cancelled` status — terminal like `done` — and reach it from every status a run may be abandoned in. A `superstates:` group does that in one place: it lists the non-terminal statuses and the `to: cancelled` transition they all inherit, so `booping playbook-transition <name> cancelled …` is legal wherever the run stands without an edge repeated per status. Hooks declared on that transition fire like any other's — stamp the artifact, commit the vault — and the printed mutation report is the record. Once cancelled the run is over: `playbook-state` reports the terminal status with no edges leaving it. +A run does not have to reach its success terminal: declare a `cancelled` status — terminal like `done` — and reach it from every status a run may be abandoned in. A `superstates:` group does that in one place: it lists the non-terminal statuses and the `to: cancelled` transition they all inherit, so `booping playbook-transition <name> cancelled …` is legal wherever the run stands without an edge repeated per status. Hooks declared on that transition fire like any other's — stamp the artifact, commit the vault. Once cancelled the run is over: `playbook-state` reports the terminal status with no edges leaving it. ### How state renders -A playbook with `states:` renders a `## State` section right after `## Playbook Steps`: the `playbook-state` invocation for that playbook, then per machine its referencing scopes, artifact path, initial status, the exact `playbook-transition` invocation (with `--state` / `--instance` where the machine needs them), and a status → `to` / `when` / `gates` table. A playbook without `states:` renders no such section. +A playbook with `states:` renders a `## State` section right after `## Playbook Steps`: the `playbook-state` invocation for that playbook, then per machine its referencing scopes, artifact path, initial status, the exact `playbook-transition` invocation (with `--state` / `--instance` / `--target` where the machine needs them), and a status → `to` / `when` / `gates` table. A playbook without `states:` renders no such section. ## Notices @@ -403,7 +396,7 @@ Create the directory under your home vault root: ~/Claude/_playbooks/my-playbook/second/prompt.md ``` -Fill in `playbook.md` with the identity frontmatter and a plain-markdown preamble body, and `playbook.yaml` with the `graph:` (plus `state:` / `states:` if the run should be resumable). Write each `<step>/prompt.md` with its own frontmatter and prompt body. Run `/playbook` in any project and it appears in the listing as a global playbook. +Fill in `playbook.md` with identity frontmatter and a plain-markdown preamble body, `playbook.yaml` with the `graph:` (plus `state:` / `states:` if the run should be resumable), and each `<step>/prompt.md` with its own frontmatter and prompt body. Run `/playbook` in any project and it appears in the listing as a global playbook. To skip the by-hand part, scaffold the skeleton — `playbook.md`, an empty `playbook.yaml` graph, and `_references/`: @@ -424,7 +417,7 @@ Same shape, but under the Project Vault: {vault}/_playbooks/my-playbook/<step>/prompt.md ``` -It appears in `/playbook` as a project playbook. Pick a `name` no other level already uses — sharing one is a name clash and neither runs until you rename. +It appears in `/playbook` as a project playbook. Pick a `name` no other level already uses. ## Worked example @@ -453,7 +446,7 @@ graph: publish: [lint, tests] ``` -Each step directory (`prep/`, `lint/`, `tests/`, `publish/`) holds a `prompt.md` with its own frontmatter and prompt body. `lint` and `tests` both depend only on `prep`, so they run together; `publish` waits for both. +Each step directory (`prep/`, `lint/`, `tests/`, `publish/`) holds a `prompt.md` with its own frontmatter and prompt body. The rendered procedure lists the steps as: @@ -476,7 +469,7 @@ The composed output carries a `## Lessons` section when any [lesson](#lessons) t These flags help while authoring: -- `--step <step>` — print just that step's body (rendered, for a `jinja: true` playbook), with no heading, instruction bullets, or gate wrapping, followed by the lessons targeting that step. This is the command every composed step section points at: each delegated step runs it itself from its bootstrap prompt, the driver runs it for inline steps, and it is handy for eyeballing one prompt in isolation. +- `--step <step>` — print just that step's body (rendered, for a `jinja: true` playbook), with no heading, instruction bullets, or gate wrapping, followed by the lessons targeting that step. This is the command every composed step section points at, and it is handy for eyeballing one prompt in isolation. - `--no-lessons` — drop the `## Lessons` section from the composed output, the step-lesson append from `--step`, and the lesson notices, leaving the bodies alone. - `--project <path>` — resolve context against the Project Vault at `<path>` instead of whatever project is attached to the current directory. Lets you render a `requires_project` or `jinja: true` playbook from anywhere. It **pins the render to the core and project levels**: the config merge skips the global tier (`~/.config/booping/config.yaml`), and discovery — for playbooks and [lessons](#lessons) alike — skips the global roots (`<home_dir>/_playbooks/`, `<home_dir>/_lessons/`), so the same command renders the same bytes on another machine. - `--set <dotted.key>=<value>` — override one config value for this render only (repeatable, later pairs win, values stay strings). The pair wins over every config tier, which is how a partial is parameterised from the command line — and how a timestamp a template reads from config gets pinned to a fixed value for a reproducible render. diff --git a/documentation/project_config.md b/documentation/project_config.md index 91814a60..b588f962 100644 --- a/documentation/project_config.md +++ b/documentation/project_config.md @@ -6,7 +6,7 @@ booping reads a single structured config — `src/config.yaml` in the plugin — The whole config lives under exactly two top-level keys: -- **`home_dir`** — the vault-home base. Top-level because it resolves *where the vault is*, before any namespace inside the config is reachable. See [The global tier](#the-global-tier). +- **`home_dir`** — the vault-home base, resolved *before* any namespace inside the config is reachable. See [The global tier](#the-global-tier). - **`core`** — everything the shipped playbook set owns. Inside `core` there is one placement rule: @@ -18,10 +18,10 @@ Inside `core` there is one placement rule: `{name}` is the playbook's name with `-` replaced by `_` — `groom` → `core.groom_playbook`, `code-review` → `core.code_review_playbook`. The rule applies literally, including to the three scaffold trees. -**`core` is the worked example your own playbooks copy.** A playbook you write declares its own namespace the same way and reads it with `{{ config.core.my_playbook.… }}` (or `{{ config.my_namespace.… }}` to sit outside `core` entirely). The placement rule travels with the copy: a key only your playbook reads sits in its `core.{name}_playbook` block; a key several of your playbooks share sits directly under `core`. +**`core` is the worked example your own playbooks copy.** A playbook you write declares its own namespace the same way and reads it with `{{ config.core.my_playbook.… }}` (or `{{ config.my_namespace.… }}` to sit outside `core` entirely). The placement rule travels with the copy. !!! note "Nothing is validated" - There is no schema gate, no unknown-field warning, and no key restricted to a particular tier. A config declaring an invented `core.my_playbook.whatever` block loads unchanged from any tier, and a project-tier `core.macros` entry is as first-class as a shipped one — it resolves at render time like the rest of the merge. Override a `core.*` key the plugin ships and getting it right is yours. + There is no schema gate, no unknown-field warning, and no key restricted to a particular tier. An invented `core.my_playbook.whatever` block loads from any tier, and a project-tier `core.macros` entry is as first-class as a shipped one. Override a `core.*` key the plugin ships and getting it right is yours. ## Snapshot: `src/config.yaml` @@ -70,6 +70,10 @@ core: groom_playbook: cross_review_agent: null + scaffold: + index.md: | + ...plan identity frontmatter + title heading, seeded with --set title=/type=... + request.md: "" agents: booping-researcher: internal: true @@ -87,7 +91,9 @@ core: where: status:in: [ready-for-dev, in-progress, awaiting-retro, awaiting-learning, done, fail, cancelled] sort: "-created" - columns: [status, title, summary, active_minutes, models] + columns: [status, title, summary, metrics_active_minutes, metrics_models, + metrics_tokens_input, metrics_tokens_output, + metrics_tokens_cache_creation, metrics_tokens_cache_read] develop_playbook: git: @@ -189,12 +195,12 @@ core: ### `core.sprint` -Sprint sizing thresholds and the story-point scale. Drives the groom playbook's split proposals, the per-task re-decompose gate, and the develop playbook's milestone grouping. +Sprint sizing thresholds and the story-point scale. - **`core.sprint.default_threshold_sp`** — soft cap on total SP per plan. Above this, groom proposes splitting the plan into sibling stubs. Default: `35`. - **`core.sprint.redecompose_threshold`** — per-task SP value at or above which groom must re-decompose the task before the run can leave `drafting`. Default: `5`. - **`core.sprint.max_milestones_per_agent`** — cap on consecutive milestones grouped into one `booping-developer` briefing by the develop playbook. Default: `2`. -- **`core.sprint.scale`** — the 1–5 SP definitions rendered into groom's body. Each entry is `{sp, meaning}`. Replace wholesale to redefine the scale — lists merge by replacement. +- **`core.sprint.scale`** — the 1–5 SP definitions rendered into groom's body. Each entry is `{sp, meaning}`. Replace wholesale to redefine — lists merge by replacement. ### `core.task_types` @@ -202,11 +208,11 @@ The task-type taxonomy groom classifies every request against. Each entry is `{t ### `core.research_agent` -The agent id an **assisted** playbook step delegates its bulk reads to. Default `booping:booping-researcher`. Point it at any agent id — a shipped worker or an external agent you registered — and every assisted step's delegated reads route there; nothing else about the steps changes. It sits directly under `core` because more than one playbook reads it (groom's two research steps, retro's session-log mining and issue research). +The agent id an **assisted** playbook step delegates its bulk reads to. Default `booping:booping-researcher`. Point it at any agent id — a shipped worker or an external agent you registered — and every assisted step's delegated reads route there; nothing else changes. Directly under `core` because more than one playbook reads it (groom's two research steps, retro's session-log mining and issue research). ### `core.plans.glob` -The plan shape as data: an ordered list of vault-relative glob patterns, defaulting to the single entry `plans/*/index.md`. All plan discovery goes through this list — patterns are tried in order and the first to claim a slug wins, so a vault carrying more than one plan shape lists them all and order decides ties. A vault laid out differently edits this one key. It is also the fallback that every [query spec](#query-specs) without its own `glob` inherits. +The plan shape as data: an ordered list of vault-relative glob patterns, default `plans/*/index.md`. All plan discovery goes through it — patterns are tried in order, the first to claim a slug wins. A vault laid out differently edits this one key, and every [query spec](#query-specs) without its own `glob` inherits it. ### `core.macros` @@ -225,7 +231,7 @@ A body calls one with `{{ macro('core.macros.date', '+%H:%M') }}`. The `command: For a reproducible render, pin a macro instead of executing it: `--stub-macro DOTTED.PATH=LITERAL` on `booping render`, `booping render-playbook` and `booping scaffold` makes the named macro return the literal without running its command (e.g. `--stub-macro core.macros.date=19700101-00-00`). The flag is repeatable, later pins winning. -Macros are honoured in every tier, project included — a project-tier entry resolves at render time exactly like a shipped one. A project config arrives with a `git clone`, so treat an unfamiliar vault's `core.macros` block the way you would treat any other executable content in a repo. +Macros are honoured in every tier, project included. A project config arrives with a `git clone`, so treat an unfamiliar vault's `core.macros` block as executable content in a repo. ## Per-playbook keys, under `core.{name}_playbook` @@ -235,12 +241,12 @@ Each shipped playbook owns one block: Delegation guidance rendered into that playbook's "Available Agents" table via the shared `playbooks/_partials/playbook_agents.md` partial. Each entry has `good_for` (bullets describing when to delegate) and an optional `bad_for` (when not to). Populated for `groom`, `develop`, `retro`, and `code-review`. -- **`core.{name}_playbook.agents.<id>.internal`** — `true` on booping's built-in workers (`booping-developer`, `booping-researcher`), marking an entry plugin-owned so it can be hidden when the block opts out of built-ins. Self-contained global agents you register omit this flag. +- **`core.{name}_playbook.agents.<id>.internal`** — `true` on booping's built-in workers (`booping-developer`, `booping-researcher`), marking an entry plugin-owned so it can be hidden when the block opts out of built-ins. Global agents you register omit it. - **`core.{name}_playbook.disable_internal_agents`** — when set, every `internal: true` entry is hidden from that table, leaving only the agents you explicitly registered. See [integrating external agents](integrating-external-agents.md). ### `core.{name}_playbook.status` -The single status this playbook claims from or reads — only `retro` declares one today, `done` (the plan status develop ends at, narrowed by a null `retro:` in the query beside it). It is a **query key**, not a lifecycle definition: point it at a different status and the playbook's picker pulls from another queue. The transitions live in the playbook's `states:` block. +The plan status this playbook's track claims from, declared as data — only `retro` declares one today, `done` (the status develop ends at, narrowed by a null `retro:` in the query beside it). It is a label, not a lifecycle definition and not a live selector: what retro's picker actually lists is the `where:` of the `queries.candidates` spec beside it, and the transitions live in the playbook's `states:` block. ### `core.{name}_playbook.queries.<id>` @@ -256,7 +262,7 @@ The agent that performs the detached second-model review of a drafted plan. **De ## Where the plan lifecycle lives -Statuses, transitions, gates and hooks are **not** in this config. Each playbook declares its own vocabulary in its `states:` block in `playbooks/<name>/playbook.yaml`, and `booping playbook-transition` is the only thing that writes a plan's `status:`. See [Playbooks → Run state](playbook.md#run-state) for the machine format, and [Vault → `plans/`](vault.md#plans) for how the four shipped playbooks chain their vocabularies through one plan. +Statuses, transitions, gates and hooks are **not** in this config. Each playbook declares its own vocabulary in its `states:` block in `playbooks/<name>/playbook.yaml`, and `booping playbook-transition` is the only thing that writes a plan's `status:`. See [Playbooks → Run state](playbook.md#run-state) for the machine format, and [Vault → `plans/`](vault.md#plans) for how groom and develop chain their vocabularies through one plan while the retro and code-review tracks join it through frontmatter instead. To reshape a status flow, edit that playbook's `states:` — or fork the playbook under a new name. No config key overrides it. @@ -280,7 +286,7 @@ Every key is optional: | `sort` | Frontmatter field name, `-` prefixed for descending. Valueless rows come last. Omitted → slug order. | | `columns` | Projection, declared order preserved. `path` and `slug` are always present. | -Any other key is a validation error naming the offending key, so a typo is caught rather than silently ignored. +Any other key is a validation error naming the offending key. `where` and `--where` share one fixed operator vocabulary — not an expression language: @@ -296,7 +302,7 @@ Clauses are repeatable and all of them apply. A row whose frontmatter lacks the The CLI mirrors every spec key inline: `--where` (repeatable), `--sort FIELD`, and `--columns A,B` override the resolved spec for that call; `--glob PATTERN` (repeatable and ordered, mutually exclusive with `--config`) queries ad hoc without any spec; `--output` picks the format — `table` (the default), `json`, `yaml`, or `paths`. -A spec lives beside its consumer, never in a central registry. Most shipped specs read the vault's plans; `core.learn_playbook.queries.candidates` overrides `glob` to read `retrospectives/*.md` instead, and `core.migrate_playbook.queries.pending` is the one that reads outside the vault entirely — it declares `root: core` and lists the migrations the plugin itself ships. +A spec lives beside its consumer, never in a central registry. Most shipped specs read the vault's plans; `core.learn_playbook.queries.candidates` overrides `glob` to read `retrospectives/*.md` instead, and `core.migrate_playbook.queries.pending` reads outside the vault entirely — `root: core`, listing the migrations the plugin itself ships. ## Scaffold trees @@ -306,9 +312,9 @@ Any mapping in the merged config — addressed by its dotted path, like a query bin/booping scaffold <config-path> <dest> [--force] [--set KEY=VALUE]... [--stub-macro DOTTED.PATH=LITERAL]... ``` -`<config-path>` is a dotted path into the merged config — the value there *is* the destination directory's contents, no wrapper key. `<dest>` is created with its parents when missing, and a destination that already holds some of the tree's files is fine: **a file that exists is skipped**, reported as `skipped existing file {path}` and left byte-for-byte alone. `--force` turns that skip into an overwrite of the files the tree names; it never deletes a directory and never touches a path the tree does not name. Exit 0 on success, 1 on a user error (unknown path, malformed tree, bad `--set`, Jinja error in seed content), 2 if a write fails at the OS level. The whole tree is rendered in memory first, so an error leaves the filesystem untouched. +`<config-path>` is a dotted path into the merged config — the value there *is* the destination directory's contents, no wrapper key. `<dest>` is created with its parents when missing. **A file that exists is skipped**, reported as `skipped existing file {path}` and left byte-for-byte alone; `--force` turns that skip into an overwrite of the files the tree names, never deleting a directory and never touching a path the tree does not name. Exit 0 on success, 1 on a user error (unknown path, malformed tree, bad `--set`, Jinja error in seed content), 2 if a write fails at the OS level. The whole tree is rendered in memory first, so an error leaves the filesystem untouched. -**Stdout contract.** For every file the run actually wrote, scaffold prints a unified diff — `--- /dev/null` (a new file) or `--- {path}` (an overwrite), then `+++ {path}` and the hunks. A file whose rendered content matches what is already on disk, and a file skipped because it exists, produce no diff; created directories keep their one-line `created dir {path}` report, and the run still ends with the `scaffolded N paths — …` count line. `booping frontmatter-update` shares this contract: the diff of the change it made on stdout, nothing at all when the file did not change, its `updated {path}: {keys}` summary and any errors on stderr. Its arguments, flags and exit codes are unchanged; it writes scalars with their YAML type, so ints, floats, booleans and `null` land unquoted and everything else lands as a string. +**Stdout contract.** For every file it actually wrote, scaffold prints a unified diff — `--- /dev/null` (a new file) or `--- {path}` (an overwrite), then `+++ {path}` and the hunks. A file whose rendered content matches what is already on disk, and a file skipped because it exists, produce no diff; created directories keep their one-line `created dir {path}` report, and the run still ends with the `scaffolded N paths — …` count line. `booping frontmatter-update` shares this contract: the diff of the change it made on stdout, nothing at all when the file did not change, its `updated {path}: {keys}` summary and any errors on stderr. It writes scalars with their YAML type — ints, floats, booleans and `null` unquoted, everything else a string. How a node is read: @@ -336,7 +342,7 @@ Trees ride the same core → global → project merge as everything else here, s - **`core.setup_playbook.scaffold`** — the project vault: `plans/`, `retrospectives/`, `codereviews/`, `_lessons/`, `notes/`, plus the seeded `sprints.md` Obsidian Bases fence and a `.gitignore`. Takes no `--set` variables. -- **`core.groom_playbook.scaffold`** — one plan directory: `index.md` seeded with the plan's identity frontmatter (the sole definition of that frontmatter) plus the title as an `#` heading, and an empty `request.md`. Takes `--set title=` and `--set type=`. +- **`core.groom_playbook.scaffold`** — the tree groom seeds every new plan from, in one `booping scaffold` call: a plan directory holding `index.md` (the plan's identity frontmatter — the sole definition of it, `commit:` stamped with the repo's HEAD at creation — plus the title as an `#` heading) and an empty `request.md`. Takes `--set title=` and `--set type=`. ``` bin/booping scaffold core.groom_playbook.scaffold \ @@ -346,15 +352,15 @@ Trees ride the same core → global → project merge as everything else here, s ## Review templates -The code-review playbook picks its review template by name, and the template set layers across the same three levels as the config itself: the plugin ships a core set, the global level adds machine-wide templates at `<home_dir>/review_templates/`, and the Project Vault's `review_templates/` speaks for one project. Later levels override by name — a global or project template file named like a shipped one replaces it; a new name adds a template to the set. +The code-review playbook picks its review template by name, and the template set layers across the same three levels as the config itself: a shipped core set, machine-wide templates at `<home_dir>/review_templates/`, and the Project Vault's `review_templates/`. Later levels override by name — a template file named like a shipped one replaces it; a new name adds to the set. ## Plan frontmatter: `summary` -Each plan file carries a `summary` field in its YAML frontmatter — a one-line statement of the plan's intent (≤ ~120 characters). Groom writes it when drafting the plan; it is the label that surfaces in `sprints.md` and makes plans searchable across the vault. +Each plan file carries a `summary` field in its YAML frontmatter — a one-line statement of the plan's intent (≤ ~120 characters). Groom writes it when drafting; it is the label that surfaces in `sprints.md`. ## The global tier -Machine-wide defaults live at `${XDG_CONFIG_HOME:-~/.config}/booping/config.yaml` (typically `~/.config/booping/config.yaml`; `XDG_CONFIG_HOME` is honoured when set). It deep-merges over the plugin's `src/config.yaml` and is in turn overridden by the per-project file — **core → global → project**. Any key valid in `src/config.yaml` is valid here; the merge rules are identical (dict keys merge, list keys replace wholesale). +Machine-wide defaults live at `${XDG_CONFIG_HOME:-~/.config}/booping/config.yaml` (typically `~/.config/booping/config.yaml`). It deep-merges over the plugin's `src/config.yaml` and is in turn overridden by the per-project file — **core → global → project**. Any key valid in `src/config.yaml` is valid here; the merge rules are identical (dict keys merge, list keys replace wholesale). The global tier's headline key is **`home_dir`** — the vault-home base under which per-project vaults are scaffolded and resolved (`<home_dir>/<project>/`). It is a raw string (`~` is expanded at vault-resolution time, e.g. `~/Claude/`). @@ -371,7 +377,7 @@ Drop a YAML file at `~/Claude/{project}/config.yaml` to override or extend the p - **Dict keys merge.** A project key is added or replaces the plugin's value; sibling keys the project file does not mention fall through unchanged. - **List keys replace wholesale.** Set `core.sprint.scale` or `core.develop_playbook.git.branches` and the project list replaces the plugin list entirely — there is no element-level merge. - **`agents` is shallow-merged.** Each agent entry is atomic: an override changing one field of an entry must restate the rest of that entry. -- **No rebuild required.** The merge happens at skill-load time, every time. Edit, save, run a skill — the new values are live. +- **No rebuild required.** The merge happens at skill-load time, every time — edit, save, and the new values are live. ### Example: tweak the SP threshold and add a branch convention @@ -394,7 +400,7 @@ core: After the next render, groom proposes splitting at 25 SP instead of 35, and develop picks `docs/` for plans typed `docs`. -Because lists replace wholesale, the project file must include every branch entry it wants to keep — omitting a row removes it. Dict keys behave the opposite way: `core.sprint.default_threshold_sp: 25` does not affect `core.sprint.redecompose_threshold` or `core.sprint.scale`, which fall through from the plugin defaults. +Because lists replace wholesale, the project file must include every branch entry it wants to keep — omitting a row removes it. Dict keys behave the opposite way: `default_threshold_sp: 25` leaves `core.sprint.redecompose_threshold` and `core.sprint.scale` falling through from the plugin defaults. ### Example: config for a playbook you wrote @@ -414,4 +420,4 @@ The playbook's `jinja: true` bodies then read `{{ config.core.ship_playbook.regi ## Verifying the merged config -Run `bin/booping debug-context` from the project directory (the one with the `.booping` marker) to dump the assembled context — including the merged config — as YAML: the authoritative answer to what a skill is actually seeing. For a single value, `bin/booping config-get <dotted.key>` prints that key resolved across the core → global → project merge (scalars as raw text, mappings/lists as YAML), and works outside any project too (core + global only). +Run `bin/booping debug-context` from the project directory (the one with the `.booping` marker) to dump the assembled context — including the merged config — as YAML: the authoritative answer to what a skill is actually seeing. For a single value, `bin/booping config-get <dotted.key>` prints that key resolved across the merge (scalars as raw text, mappings/lists as YAML), and works outside any project too (core + global only). diff --git a/documentation/vault.md b/documentation/vault.md index 1f46e941..16dfa6f2 100644 --- a/documentation/vault.md +++ b/documentation/vault.md @@ -1,22 +1,22 @@ # Vault -Every booping project gets its own Project Vault, scaffolded by the [setup playbook](install.md). By default it lives at `~/Claude/{project}/`; set the `.booping` marker's `vault_path:` key to keep it **inside the repo** instead (a local vault). The vault is plain markdown with YAML frontmatter — open the directory in Obsidian for graph view and backlinks across plans, retros, and lessons. +Every booping project gets its own Project Vault, scaffolded by the [setup playbook](install.md). By default it lives at `~/Claude/{project}/`; set the `.booping` marker's `vault_path:` key to keep it **inside the repo** instead (a local vault). Plain markdown with YAML frontmatter — open the directory in Obsidian for graph view and backlinks across plans, retros, and lessons. -This page is the reference for every file and directory inside the vault. For the lifecycle that ties them together, start at [Quick start](quick_start.md). +Reference for every file and directory inside the vault. For the lifecycle that ties them together, start at [Quick start](quick_start.md). ## `plans/` -A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md` — the shape the groom playbook authors. The `core.plans.glob` config key governs discovery: an ordered list of globs, `plans/*/index.md` as shipped, so the plan shape is data rather than engine logic — a vault laying plans out differently edits that one key and every consumer follows. Each plan carries YAML frontmatter (status, type, story points, a one-line `summary`, etc.) and a body of milestones with tasks. Authored by the [groom playbook](groom.md), executed by the [develop playbook](develop.md). +A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md`, authored by the [groom playbook](groom.md) and executed by the [develop playbook](develop.md). Frontmatter (status, type, story points, a one-line `summary`) plus a body of milestones with tasks. Discovery follows the `core.plans.glob` config key — an ordered list of globs, `plans/*/index.md` as shipped — so a vault laying plans out differently edits that one key and every consumer follows. -A plan's `status:` is **run state**, not a shared lifecycle: `index.md` doubles as the run artifact of whichever playbook is operating on the plan, so the status vocabulary is the one that playbook declares in its own `states:` block. Groom ends at `ready-for-dev`; develop claims from there and ends at `done` — or at `fail` when a blocker survives two fix attempts, or `cancelled` when you call the run off. All three are terminal: the end of the plan lifecycle. Only `booping playbook-transition` writes the status, and it refuses any move the machine does not declare. See [Playbooks → Run state](playbook.md#run-state). +A plan's `status:` is **run state**, not a shared lifecycle: `index.md` doubles as the run artifact of whichever playbook operates on it, so the vocabulary is the one that playbook declares in its own `states:` block. Groom ends at `ready-for-dev`; develop claims from there and ends at `done`, at `fail` when a blocker survives two fix attempts, or at `cancelled` when you call the run off. All three are terminal. Only `booping playbook-transition` writes the status, and it refuses any move the machine does not declare. See [Playbooks → Run state](playbook.md#run-state). -Two frontmatter keys carry a finished plan into the tracks that run beside the lifecycle rather than inside it. `retro:` is null until a retrospective covers the plan, then holds that file's path (or `skipped`) — that null is the retro queue. `code_reviews:` is a **list**: seeded null, then appended with the vault-relative path of every review that closes on the plan, so it reads as review history rather than a queue flag. The code-review queue is every plan at `done`, reviewed or not. +Two frontmatter keys carry a finished plan into the tracks that run beside the lifecycle. `retro:` is null until a retrospective covers the plan, then holds that file's path (or `skipped`) — that null is the retro queue. `code_reviews:` is a **list**: seeded null, then appended with the vault-relative path of every review that closes on the plan — review history, not a queue flag. The code-review queue is every plan at `done`, reviewed or not. -Seven further keys carry session metrics. `sessions:` is the list of Claude Code session ids that groomed and developed the plan, appended by transition hooks on the groom and develop edges (a run started outside a Claude Code session adds nothing). When develop closes the plan, six flat `metrics_`-prefixed keys are stamped from those transcripts — `metrics_active_minutes:` (whole minutes of active work), `metrics_models:` (the sorted distinct model ids that ran them), and the token totals `metrics_tokens_input:`, `metrics_tokens_output:`, `metrics_tokens_cache_creation:`, `metrics_tokens_cache_read:`. They are flat rather than a nested `metrics:` mapping because Obsidian Properties and Bases cannot address a nested mapping as a column, and all six surface as columns in `sprints.md`. `bin/booping session-stats {vault}/plans --mask index.md` recomputes them at any time — it stamps by default, `--force` overwrites existing values and `--dry-run` prints the same JSON without writing. +Seven further keys carry session metrics. `sessions:` lists the Claude Code session ids that groomed and developed the plan, appended by transition hooks on the groom and develop edges (a run started outside a Claude Code session adds nothing). When develop closes the plan, six flat `metrics_`-prefixed keys are stamped from those transcripts: `metrics_active_minutes:` (whole minutes of active work), `metrics_models:` (the sorted distinct model ids that ran them), and the token totals `metrics_tokens_input:`, `metrics_tokens_output:`, `metrics_tokens_cache_creation:`, `metrics_tokens_cache_read:`. Flat rather than a nested `metrics:` mapping because Obsidian Properties and Bases cannot address a nested mapping as a column; all six surface as columns in `sprints.md`. `bin/booping session-stats {vault}/plans --mask index.md` recomputes them at any time — it stamps by default, `--force` overwrites existing values and `--dry-run` prints the same JSON without writing. -Active time is turn time minus every interval the run spent blocked on a human: an `AskUserQuestion` tool call up to its matching `tool_result`, and any span ending in a user rejection. System decisions (`permission-rule`, `automode-blocked`, `automode-unavailable`) are never subtracted. **The metric under-reports wait**: a tool call auto-approved by a permission rule and one a human approved after ten minutes are structurally identical in the transcript — no field distinguishes them — so that wait stays inside active time rather than being guessed at. +Active time is turn time minus every interval the run spent blocked on a human: an `AskUserQuestion` tool call up to its matching `tool_result`, and any span ending in a user rejection. System decisions (`permission-rule`, `automode-blocked`, `automode-unavailable`) are never subtracted. **The metric under-reports wait**: a tool call auto-approved by a permission rule and one a human approved after ten minutes are structurally identical in the transcript — no field distinguishes them — so that wait stays inside active time. -Sibling stubs created by a groom-driven split point at the primary plan via `split_from: plans/...` in their frontmatter. +When a groom run splits an oversized sprint, only the first slice stays in this plan; the rest are parked as sibling stubs, each groomed in its own run. ## `retrospectives/` @@ -28,29 +28,31 @@ That `status:` is the retro track's run state — `awaiting-retro → awaiting-l ## `codereviews/` -One file per code-review run, written by the [code-review playbook](code_review.md), grouped by what was reviewed: `codereviews/{plan-dirname}/{YYYYMMDDHHmm}.md` when a plan is in scope, `codereviews/{target-slug}/{YYYYMMDDHHmm}.md` for an ad-hoc scope such as the latest commits. Scaffolded for new vaults and created lazily in older ones, so an existing vault needs no migration. +One file per code-review run, written by the [code-review playbook](code_review.md), grouped by what was reviewed: `codereviews/{plan-dirname}/{YYYYMMDDHHmm}.md` when a plan is in scope, `codereviews/{target-slug}/{YYYYMMDDHHmm}.md` for an ad-hoc scope such as the latest commits. Scaffolded for new vaults and created lazily in older ones — no migration needed. The file records `## Scope`, `## Findings`, `## Verdict` and `## Resolution`. Its frontmatter carries `plan:` — the reviewed plan's vault-relative path, or `null` — and its own `status:`, the code-review track's run state: `in-agent-review → human-review → done`. That status is the review's, never a plan's: the reviewed plan stays at `done` and only gains the review's path in its `code_reviews:` list. The playbook runs with the vault root as its workdir and addresses the file with `--target codereviews/{dir}/{ts}.md`. ## `_lessons/` -Targeted lessons — durable rules accumulated over many sprints, and the only file surface the [learn playbook](learn.md) writes inside the Project Vault. Lessons live in exactly two flat roots: this directory, scoped to the project, and its machine-wide sibling at `<home_dir>/_lessons/` (default `~/Claude/_lessons/`), which applies to every project — a file of the same name here shadows the global one. Files are named `{N}_{title}.md`, `N` a monotonic counter keeping each directory chronologically ordered. +Durable rules accumulated over many sprints, and the only file surface the [learn playbook](learn.md) writes inside the Project Vault. Lessons live in exactly two flat roots: this directory, scoped to the project, and its machine-wide sibling `<home_dir>/_lessons/` (default `~/Claude/_lessons/`), which applies to every project — a file of the same name here shadows the global one. Files are named `{N}_{title}.md`, `N` a monotonic counter keeping each directory chronologically ordered. -Each file carries a `targets:` frontmatter list saying what it applies to: `{playbook}`, `{playbook}/{step}`, `agent:{id}`, or `skill:{name}`. That list is the only routing there is — a file with no valid `targets:` is injected nowhere. +Each file carries a `targets:` frontmatter list: `{playbook}`, `{playbook}/{step}`, `agent:{id}`, or `skill:{name}`. That list is the only routing there is — a file with no valid `targets:` is injected nowhere. + +Targeted lessons are also the only way to shape a booping skill's or agent's behaviour for a project: an `agent:{id}` or `skill:{name}` lesson renders inside that agent's or skill's body, and there is no separate extension-file mechanism. An older vault still carrying `_booping/skill_*.md` or `agent_*.md` extension files gets them converted on the next `/playbook migrate` run — each becomes a targeted lesson here, and the converted original is removed. Injected by `booping render-playbook` into the composed procedure or a step prompt, and into the bodies of booping's own agents and skills at load time. See [Playbooks → Lessons](playbook.md#lessons) for the full reference. ## `notes/` -Free-form user notes — plan-review comments, code-review threads, ideas for next sprints, anything else. **Skills and agents do not read this directory.** It is a scratchpad for you, kept in the same vault for convenience and Obsidian graph visibility. +Free-form user notes — plan-review comments, code-review threads, ideas for next sprints, anything else. **Skills and agents do not read this directory.** A scratchpad for you, kept in the same vault for convenience and Obsidian graph visibility. ## `.booping.log` -Append-only log of `booping` CLI invocations, at the vault root. Written by the CLI, read by nobody — it is there for debugging a render or a transition. The seeded `.gitignore` excludes it, so it never lands in a vault commit. +Append-only log of `booping` CLI invocations, at the vault root. Written by the CLI, read by nobody — for debugging a render or a transition. The seeded `.gitignore` excludes it, so it never lands in a vault commit. ## `plan_templates/` -Project-local plan templates. Each file has frontmatter (`name`, `description`) plus two top-level sections (`# Plan Body`, `# Quality Checklist`). Discovered by the [groom playbook](groom.md) alongside the core templates the plugin ships; a file overrides a core template by sharing its `name`, or adds an entirely new template flavour suited to the project. +Project-local plan templates. Each file has frontmatter (`name`, `description`) plus two top-level sections (`# Plan Body`, `# Quality Checklist`). Discovered by the [groom playbook](groom.md) alongside the core templates the plugin ships; a file overrides a core template by sharing its `name`, or adds a new flavour of its own. ## `review_templates/` @@ -68,9 +70,9 @@ A later tier overrides an earlier one by `name`, keeping the earlier entry's pos ## `sprints.md` -An at-a-glance view of every plan in the vault — an [Obsidian Bases](https://help.obsidian.md/bases) fence over the `plans/*/index.md` files, its columns led by status and its rows sorted newest-first by `created`. Bases resolves every path against the *Obsidian* vault root, so the seeded filter scopes itself with `file.inFolder(this.file.folder + "/plans")` — the folder of the note holding the fence — which keeps it correct when the booping vault is nested inside a larger Obsidian vault. +An at-a-glance view of every plan in the vault — an [Obsidian Bases](https://help.obsidian.md/bases) fence over the `plans/*/index.md` files, its columns led by status and its rows sorted newest-first by `created`. The column set includes the session metrics — `active_minutes`, `models` and three token columns (In, Out, Cached), those last three Bases formulas over the four `metrics_tokens_*` keys — read from the `metrics_*` frontmatter the sprint playbooks stamp on each plan (see [`plans/`](#plans)). Bases resolves every path against the *Obsidian* vault root, so the seeded filter scopes itself with `file.inFolder(this.file.folder + "/plans")` — the folder of the note holding the fence — keeping it correct when the booping vault is nested inside a larger Obsidian vault. -**Seeded once at setup (`booping scaffold core.setup_playbook.scaffold`); no run ever rewrites or regenerates it.** The one thing that touches the fence again is a shipped vault migration, and only to append a new column your existing view predates. Obsidian evaluates the query live against the plan files, so the view is never stale and needs no refresh step. Edit the fence to change columns, sorting or filters — it is yours from the moment it is written. +**Seeded once at setup (`booping scaffold core.setup_playbook.scaffold`); no run ever rewrites or regenerates it.** Only a shipped vault migration touches the fence again, and only to bring its columns current — renaming a key in place, appending a column your existing view predates. Obsidian evaluates the query live against the plan files, so the view is never stale. Edit the fence to change columns, sorting or filters — it is yours from the moment it is written. Outside Obsidian the file is an inert code block. For a machine-readable listing of the same data, use `bin/booping query`: @@ -92,7 +94,7 @@ Clauses are repeatable and all of them apply. A row whose frontmatter lacks the ## `.booping` -The marker that ties a repo to its vault. Unlike everything else on this page, `.booping` lives in the **attached repo's working tree** (its root), not inside the vault. Written by the [setup playbook](install.md), it carries `project_name: {project}` — how every skill resolves which vault to operate on. An optional `vault_path:` key resolves the vault to that path instead of `~/Claude/{project}/` (relative paths against the repo root, absolute paths and `~` honoured) — this is how a repo-local vault is wired. A third key, `latest_migration:`, is the watermark recording which of the plugin's shipped vault migrations this project has already applied, written only by `bin/booping marker-set latest_migration=<id>` as the [migrate playbook](playbook.md) finishes one. When the watermark falls behind the shipped migrations, every render stops with a notice telling you to run `/playbook migrate`; nothing else renders until the vault catches up. Commit `.booping` with the repo so the binding travels with the checkout. +The marker that ties a repo to its vault. Unlike everything else on this page it lives in the **attached repo's working tree** (its root), not inside the vault. Written by the [setup playbook](install.md), it carries three keys. `project_name: {project}` — how every skill resolves which vault to operate on. Optional `vault_path:` resolves the vault to that path instead of `~/Claude/{project}/` (relative paths against the repo root, absolute paths and `~` honoured) — this is how a repo-local vault is wired. `latest_migration:` is the watermark recording which of the plugin's shipped vault migrations this project has already applied, written only by `bin/booping marker-set latest_migration=<id>` as the [migrate playbook](playbook.md) finishes one; when it falls behind, every render stops with a notice telling you to run `/playbook migrate`, and nothing else renders until the vault catches up. Commit `.booping` with the repo so the binding travels with the checkout. ## `config.yaml` diff --git a/mkdocs.yml b/mkdocs.yml index abb052ca..0c774c1e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,7 +17,7 @@ nav: - Quick start: quick_start.md - Install: install.md - Vault: vault.md - - Playbooks (unstable): playbook.md + - Playbooks: playbook.md - Project config: project_config.md - Integrating external agents: integrating-external-agents.md - Workflow: diff --git a/vault/_lessons/0014_code-style-guide.md b/vault/_lessons/0014_code-style-guide.md index a29472f9..182d7269 100644 --- a/vault/_lessons/0014_code-style-guide.md +++ b/vault/_lessons/0014_code-style-guide.md @@ -10,4 +10,3 @@ created: 2026-08-10 Code style practices: 1. Do not use lazy (function-local) imports in Python; top-level imports only, unless there is genuinely no other way. -2. Prefer parametrized tests when testing the same surface with different inputs and expected results. diff --git a/vault/_lessons/0016_test-practices.md b/vault/_lessons/0016_test-practices.md new file mode 100644 index 00000000..d6e0186e --- /dev/null +++ b/vault/_lessons/0016_test-practices.md @@ -0,0 +1,21 @@ +--- +id: 16 +title: Plan tests per milestone and write them against real behavior +targets: + - groom/draft-plan + - agent:booping-developer +retro: null +created: 2026-08-10 +--- + +**Grooming**: every milestone names what to test — the public surface it touches, the input cases that matter, and the tier it belongs at: integration by default, unit only for pure logic with tricky branches. One line, no test code. Example: "frontmatter-update extended with conditional quoting — test the CLI surface with parametrized inputs: plain, spaced, colon-bearing values." + +**Practices**: + +1. Name the break each test catches. A test that only fails on an intentional decision (a constant's value, exact wording, source text) is a change detector — assert the behavior that depends on the decision instead. Acid test: refactor the internals while keeping the public contract identical — a test that breaks is testing the wrong thing. +2. Name the test after the outcome observed, not the mechanism — `renders the first slide on load`, not `calls setIndex(0)`. +3. Derive expected values by hand — literals or checked fixtures, never the code under test or its helpers. +4. Test your own boundary contract, not framework mechanics. +5. Parametrize when the same surface takes different inputs and expected results. +6. Mock only at a true external boundary — network, disk, clock, subprocess, third-party — never a collaborator you own. Keep what the test depends on real, mirror real structures completely, and never assert on the mock itself. Test-only cleanup lives in test utilities, not production classes. +7. Before finishing, mutate the production code mentally — wrong constant, wrong branch, missing side effect, empty return, missing validation. Each mutation should fail a test. diff --git a/vault/docs/_runs/202608101223-docs-refresh.md b/vault/docs/_runs/202608101223-docs-refresh.md index ede6a3e9..2433311c 100644 --- a/vault/docs/_runs/202608101223-docs-refresh.md +++ b/vault/docs/_runs/202608101223-docs-refresh.md @@ -18,6 +18,7 @@ documented: - plans/202608081523_session-metrics-idle-and-tokens/index.md - plans/202608091310_scaffold-seeded-plan-creation/index.md completed: 2026-08-10 14:14 +agents.record: a5d38d8587bcd9b0a --- # Docs refresh From 753920ebff42b8759d80985e25e9a4fd421eea1e Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 19:14:18 +0700 Subject: [PATCH 35/44] docs: downstream readers and docs follow the milestone files retro, code-review and the lesson-check partial read index.md plus the milestone files; the vault, develop and groom pages, README and CLAUDE.md describe the plan directory, the milestone file contract and the generated table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- CLAUDE.md | 2 ++ README.md | 4 +++- documentation/develop.md | 6 +++--- documentation/groom.md | 8 ++++---- documentation/vault.md | 6 +++++- playbooks/code-review/review/opus-5.md | 5 +++-- playbooks/retro/prepare/base.md | 2 +- src/templates/_partials/_plan_lesson_check.j2 | 2 +- 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d483641b..c5f65de3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,7 @@ One shipped skill (`/playbook`); everything procedural is a **playbook** it driv - Three-tier deep merge, **core → global → project**: `src/config.yaml` ← `${XDG_CONFIG_HOME:-~/.config}/booping/config.yaml` ← `{vault}/config.yaml`. Later tiers win; lists replace wholesale. No schema gate and no tier restriction — any key loads in any tier. - Top level is exactly `home_dir` + `core`. **Placement rule** under `core`: a key one playbook owns lives at `core.{name}_playbook`; a shared key sits directly under `core`. A user's own playbook namespace copies this shape. - Any mapping in the merged config can be a **query spec** (`booping query --config <dotted.path>`, `| query` filter) or a **scaffold tree** (`booping scaffold <dotted.path> <dest>`) — the value at the dotted path is the spec/tree, no wrapper key. Specs live beside their consumer (`core.{name}_playbook.queries.<id>`), never in a central registry. +- `core.plans` — plan shape shared by groom and develop: `glob` (plan discovery) and `milestones` (`glob` relative to the plan dir + `table_columns` for the generated table). Never restate either literal in prose. - `core.macros.<name>` — an argv list, or a mapping with `command:` plus `cwd: repo|vault` scoping; rendered bodies call them via the `macro()` global; `--stub-macro` (or a vault `macro_stubs:` mapping) pins them for reproducible renders. - Full reference: [documentation/project_config.md](documentation/project_config.md). @@ -61,6 +62,7 @@ Multi-step guided procedures discovered from three roots — core `playbooks/`, Each playbook owns its own status vocabulary in its `states:` block — there is no shared lifecycle, and those blocks are the authoritative sets. - **Plan track** (`groom` + `develop`): the plan is a directory `{vault}/plans/{slug}/` whose `index.md` is both run artifact and plan document; groom ends at `ready-for-dev`, develop at `done` | `fail`, both also `cancelled`. Shape is the `core.plans.glob` config key (`plans/*/index.md`). +- **Milestone files**: each milestone is a file under the plan directory (`core.plans.milestones.glob`, `milestones/*.md`), frontmatter `id`/`title`/`sp`/`status`/`plan` plus tasks, DoD and Verify. It is the contract handed to a worker by path — `index.md` is context only, its `## Milestones` table (`core.plans.milestones.table_columns`) and the plan's `sp` are generated. Milestone `status:` is develop's per-instance run state (`pending` → `in-progress` → `done`, `blocked` off in-progress); every edge runs the `refresh-milestone-table` hook. - **Retro track** (`retro` + `learn`): artifact is a standalone `{vault}/retrospectives/{slug}.md` (`awaiting-retro` → `awaiting-learning` → `done`); plans stay at `done` throughout. Addressed with `--target` since the machines declare no `artifact:`. - **Code-review track** (`code-review`): artifact is `{vault}/codereviews/{plan-dirname}/{ts}.md` (ad-hoc scopes: `codereviews/{target-slug}/{ts}.md`, `plan: null`), machine `in-agent-review` → `human-review` → `done`; plans stay at `done`. Addressed with `--target` since the machine declares no `artifact:`. - The tracks join through **plan frontmatter, not status**: `retro:` is null until covered, so `{status: done, retro: null}` is the retro queue; `code_reviews:` is a list of every review that closed on the plan — history, not a queue flag — so the review queue is every `{status: done}` plan, re-review included. diff --git a/README.md b/README.md index 1e10b73f..d1da1193 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ Candidates are listed for you if you forget the exact path. `groom` shapes the spec and waits for explicit user approval before handing off; `develop` claims the next ready plan and executes milestone by milestone; `retro` compares what shipped to the original spec; `learn` distils the retrospective into rules that bind the next sprint. +A plan is a directory, not a file: `plans/{slug}/index.md` carries the frontmatter, the approach and a generated `## Milestones` table, while each milestone is its own file under `milestones/` holding that milestone's tasks, definition of done and verification. That milestone file is what develop hands a coding agent as its contract, with `index.md` alongside as context. + There is no shared status table — **each playbook has its own status vocabulary** and advances its own run artifact through it. Groom and develop run on the plan, so its `status:` frontmatter is whatever those two last wrote, and the plan track ends when develop closes it — at `done`, or at `fail` or `cancelled`. Retro and learn are a **separate track** over a standalone retrospective under `retrospectives/`; code review is a third, over one file per run under `codereviews/`. The statuses those tracks write are their own artifact's, never the plan's. ```text @@ -115,7 +117,7 @@ A plan's frontmatter status is written by groom or develop. Retro and learn writ - **`framing`** — clarifying the request and settling scope. - **`researching`** — the blast-radius and web-research passes are running. -- **`drafting`** — design is being settled with you in conversation and written into the plan body. +- **`drafting`** — design is being settled with you in conversation and written into `index.md` and one file per milestone. - **`cross-reviewing`** — a second model is reviewing the draft (skipped when no reviewer is configured). - **`presenting`** — the approval screen is on the table. - **`awaiting-approval`** — waiting for your explicit approval or change request; groom's single review gate. diff --git a/documentation/develop.md b/documentation/develop.md index 41b24094..51fc74cf 100644 --- a/documentation/develop.md +++ b/documentation/develop.md @@ -10,7 +10,7 @@ Development is a **playbook**, not a skill — driven by [`/playbook`](playbook. ## What it does -The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own — every playbook declares its own. Run state lives in the plan's own `index.md`, so a stopped sprint is **resumable**. +The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own — every playbook declares its own. Run state lives in the plan's own `index.md`, and each milestone's `pending → in-progress → done` state in its own file under `milestones/`, so a stopped sprint is **resumable**. Five steps, in dependency order: @@ -18,7 +18,7 @@ Five steps, in dependency order: |------|--------------| | `intake` | Resolve the plan — one entering at `awaiting-approval` is advanced to `ready-for-dev` without asking, since handing it to develop is the approval — and check the plan against the repo's current shape (drift) | | `provision` | Pick and confirm the sprint branch, then settle the milestone groups the briefings will cover | -| `develop-loop` | Brief `booping-developer` per group, verify each milestone's DoD, commit as it goes | +| `develop-loop` | Brief `booping-developer` per group — milestone files as the contract, `index.md` as context — verify each milestone's DoD, commit as it goes | | `verify` | Run the project's lint / typecheck / test gates plus the plan's Final Verification | | `wrap-up` | Closing commit, sprint report, transition to `done` | @@ -35,7 +35,7 @@ Every milestone group runs in **one session**. Bare invocation lists the vault's plans at `ready-for-dev` or `awaiting-approval` and asks you to pick one. Name a plan path to target a specific one. -To resume a sprint already `in-progress`, invoke the playbook against the same plan: `playbook-state` reports the frontier and the run picks up at the first milestone whose DoDs are not all `[x]`. +To resume a sprint already `in-progress`, invoke the playbook against the same plan: `playbook-state` reports the frontier and the run picks up at the first milestone file whose `status:` is not `done`. Each transition also regenerates `index.md`'s `## Milestones` table and re-sums the plan's story points, so the table is a view, never the source of truth. ## Branch diff --git a/documentation/groom.md b/documentation/groom.md index cdc082aa..b8598227 100644 --- a/documentation/groom.md +++ b/documentation/groom.md @@ -1,6 +1,6 @@ # groom playbook -Spec a sprint: take a rough request and produce a reviewable plan under `~/Claude/{project}/plans/{slug}/index.md` with milestones, tasks, story points, and definitions of done. +Spec a sprint: take a rough request and produce a reviewable plan under `~/Claude/{project}/plans/{slug}/` — an `index.md` plus one file per milestone under `milestones/`, carrying tasks, story points, and definitions of done. Grooming is a **playbook**, not a skill — it is driven by [`/playbook`](playbook.md): @@ -14,9 +14,9 @@ Run states: `framing` → `researching` → `drafting` → `cross-reviewing` → Almost every step runs in your session — inline, or assisted (heavy reads go to the research agent, which returns a bounded summary). The exception is `cross-review`, which hands the drafted plan to a second-model reviewer and does nothing unless you name one. `present` is the run's single approval gate. -The output is a plan directory `~/Claude/{project}/plans/{slug}/` whose `index.md` carries the plan and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads. Groom creates the directory with one `booping scaffold` call from a config-declared tree, so the fresh `index.md` arrives complete — including a real `commit:` stamped with repo HEAD at creation. The intake briefing and any web-research notes land beside it. The directory doubles as the run workdir, so a groom run is **resumable**: its run state lives in the same `index.md`. +The output is a plan directory `~/Claude/{project}/plans/{slug}/`. Its `index.md` carries the approach, the scope and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads; each milestone is a file of its own under `milestones/`, and that file — not a section of the index — is what develop hands a coding agent as its contract. `index.md`'s `## Milestones` table and the plan's total story points are generated from those files, never hand-kept. Groom creates the directory with one `booping scaffold` call from a config-declared tree, so the fresh `index.md` arrives complete — including a real `commit:` stamped with repo HEAD at creation, and `draft-plan` scaffolds each milestone file the same way. The intake briefing and any web-research notes land beside it. The directory doubles as the run workdir, so a groom run is **resumable**: its run state lives in the same `index.md`. -A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else. A vault still holding flat `plans/{slug}.md` files converts them with `/playbook migrate`. +A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else, and `core.plans.milestones.glob` resolves the milestone files inside it. A vault still holding flat `plans/{slug}.md` files converts them with `/playbook migrate`. Six steps, in dependency order: @@ -25,7 +25,7 @@ Six steps, in dependency order: | `intake` | Clarify the request, settle scope, scaffold the plan directory — briefing, identity frontmatter, `commit:` at repo HEAD — with one `booping scaffold` call | | `research-codebase` | Map the blast radius — files, modules, integrations, prior art (assisted: heavy reads go to the research agent) | | `research-web` | Check external practice where the design is uncertain, and verify package versions, image tags, API endpoints and CLI flags against current docs | -| `draft-plan` | Design with you in conversation, then write the plan body against a plan template | +| `draft-plan` | Design with you in conversation, then write the plan against a plan template — `index.md` plus one milestone file each | | `cross-review` | Hand the drafted plan to a second-model reviewer for severity findings — skipped unless `core.groom_playbook.cross_review_agent` names one (unset by default) | | `present` | Present approach, milestones and SP totals; the run's single approval gate | diff --git a/documentation/vault.md b/documentation/vault.md index 16dfa6f2..c4afe1b5 100644 --- a/documentation/vault.md +++ b/documentation/vault.md @@ -6,10 +6,14 @@ Reference for every file and directory inside the vault. For the lifecycle that ## `plans/` -A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md`, authored by the [groom playbook](groom.md) and executed by the [develop playbook](develop.md). Frontmatter (status, type, story points, a one-line `summary`) plus a body of milestones with tasks. Discovery follows the `core.plans.glob` config key — an ordered list of globs, `plans/*/index.md` as shipped — so a vault laying plans out differently edits that one key and every consumer follows. +A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md` and a `milestones/` subdirectory, authored by the [groom playbook](groom.md) and executed by the [develop playbook](develop.md). `index.md` carries the frontmatter (status, type, story points, a one-line `summary`), the approach and scope, and a `## Milestones` table; each milestone is a file of its own, `milestones/{nn}-{kebab-title}.md`, with frontmatter `id`, `title`, `sp`, `status`, `plan` and a body of tasks, Definition of Done checkboxes and Verify lines. That file is the contract a `booping-developer` agent is handed — develop passes its path, never its text, with `index.md` alongside as context. + +Discovery follows the `core.plans.glob` config key — an ordered list of globs, `plans/*/index.md` as shipped; the milestone files follow `core.plans.milestones.glob`, `milestones/*.md` relative to the plan directory. A vault laying plans out differently edits those keys and every consumer follows. A plan's `status:` is **run state**, not a shared lifecycle: `index.md` doubles as the run artifact of whichever playbook operates on it, so the vocabulary is the one that playbook declares in its own `states:` block. Groom ends at `ready-for-dev`; develop claims from there and ends at `done`, at `fail` when a blocker survives two fix attempts, or at `cancelled` when you call the run off. All three are terminal. Only `booping playbook-transition` writes the status, and it refuses any move the machine does not declare. See [Playbooks → Run state](playbook.md#run-state). +Each milestone file's `status:` is run state as well — `pending → in-progress → done`, plus `blocked` off `in-progress` — written only by develop's transitions. Every such move regenerates `index.md`'s `## Milestones` table and re-sums the plan's story points from the milestone files, so neither is ever hand-kept. + Two frontmatter keys carry a finished plan into the tracks that run beside the lifecycle. `retro:` is null until a retrospective covers the plan, then holds that file's path (or `skipped`) — that null is the retro queue. `code_reviews:` is a **list**: seeded null, then appended with the vault-relative path of every review that closes on the plan — review history, not a queue flag. The code-review queue is every plan at `done`, reviewed or not. Seven further keys carry session metrics. `sessions:` lists the Claude Code session ids that groomed and developed the plan, appended by transition hooks on the groom and develop edges (a run started outside a Claude Code session adds nothing). When develop closes the plan, six flat `metrics_`-prefixed keys are stamped from those transcripts: `metrics_active_minutes:` (whole minutes of active work), `metrics_models:` (the sorted distinct model ids that ran them), and the token totals `metrics_tokens_input:`, `metrics_tokens_output:`, `metrics_tokens_cache_creation:`, `metrics_tokens_cache_read:`. Flat rather than a nested `metrics:` mapping because Obsidian Properties and Bases cannot address a nested mapping as a column; all six surface as columns in `sprints.md`. `bin/booping session-stats {vault}/plans --mask index.md` recomputes them at any time — it stamps by default, `--force` overwrites existing values and `--dry-run` prints the same JSON without writing. diff --git a/playbooks/code-review/review/opus-5.md b/playbooks/code-review/review/opus-5.md index 9f61dd2d..9eb77501 100644 --- a/playbooks/code-review/review/opus-5.md +++ b/playbooks/code-review/review/opus-5.md @@ -26,8 +26,9 @@ Walk these in order. 6. **Run the three dynamic checks** no static checklist can carry. - **Lesson compliance** — every lesson loaded below, against the change. A contradiction is a `BLOCKER`; cite the lesson id. - - **Plan-DoD alignment**, when a plan is in scope — cross-reference each `[x]` DoD checkbox - against the diff. A DoD item marked done that the diff does not deliver is a finding. + - **Plan-DoD alignment**, when a plan is in scope — read `index.md` plus + `{{ config.core.plans.milestones.glob }}` beside it, and cross-reference each `[x]` DoD + checkbox against the diff. A DoD item marked done that the diff does not deliver is a finding. - **Plan-intent match**, when a plan is in scope — the mandated test methodology, structural pattern and architectural decisions. A diff that solves the problem a different way than the plan specified is a finding, not silently-accepted variation. diff --git a/playbooks/retro/prepare/base.md b/playbooks/retro/prepare/base.md index fc4c1061..062d3ee9 100644 --- a/playbooks/retro/prepare/base.md +++ b/playbooks/retro/prepare/base.md @@ -1,4 +1,4 @@ -Read each plan in the working set in full — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). +Read each plan in the working set in full — `index.md` plus `{{ config.core.plans.milestones.glob }}` beside it — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). {{ tools.render('src/templates/_partials/_lessons.j2') }} diff --git a/src/templates/_partials/_plan_lesson_check.j2 b/src/templates/_partials/_plan_lesson_check.j2 index a60a1d55..172d5975 100644 --- a/src/templates/_partials/_plan_lesson_check.j2 +++ b/src/templates/_partials/_plan_lesson_check.j2 @@ -1,4 +1,4 @@ -1. Read the plan file(s) listed in the brief in full — frontmatter, milestones, tasks, DoDs, Verify lines. +1. Read each plan listed in the brief in full — `index.md` plus `{{ config.core.plans.milestones.glob }}` beside it. 2. Cross-check the plan against the lesson set included verbatim in the brief. 3. Return a structured summary per plan with one section: - **Plan-stage lesson gaps** — places where the plan as written contradicts or omits a loaded lesson. Per item: lesson path, the rule, where in the plan it should have shown up (milestone / task / DoD / Verify), and what is there instead (or what is missing). From 2b611080438d9de5eeaf7bffad8cd1ea14b0bb39 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 19:14:18 +0700 Subject: [PATCH 36/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index 46ca6152..1e56213e 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -69,7 +69,7 @@ The milestone file is the only place a milestone's work is written down. `index. | 4 | Groom writes milestone files | 3 | done | | 5 | Milestone state machine and table refresh | 4 | done | | 6 | Develop delegates paths, not bodies | 4 | done | -| 7 | Downstream readers and documentation | 3 | pending | +| 7 | Downstream readers and documentation | 3 | done | | 8 | Reports, structure checks and eval fixtures | 2 | pending | --- @@ -254,7 +254,7 @@ The milestone file is the only place a milestone's work is written down. `index. **Note**: 6.3 landed in `src/templates/_partials/_developer_body.j2`, the only body `agents/booping-developer.md.j2` includes. -### M7: Downstream readers and documentation — 3 SP | pending +### M7: Downstream readers and documentation — 3 SP | done **Goal**: every surface that reads a plan "in full" follows the milestone files, and the hand-authored docs describe the new shape. @@ -264,20 +264,20 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 7.1 | Extend the plan read set in retro and code-review: `prepare`'s full-plan read, the lesson-check partial's "frontmatter, milestones, tasks, DoDs, Verify" instruction, and the review pass's DoD-checkbox cross-reference all name `index.md` plus `milestones/*.md`. | `playbooks/retro/prepare/base.md`, `src/templates/_partials/_plan_lesson_check.j2`, `playbooks/code-review/review/opus-5.md` | 1 | pending | -| 7.2 | Update the public docs and the repo guide: plan shape in the vault doc, develop's resume description, groom's plan description, README's plan-track narrative, and the CLAUDE.md Lifecycle bullet that defines a plan directory. | `documentation/vault.md`, `documentation/develop.md`, `documentation/groom.md`, `README.md`, `CLAUDE.md` | 2 | pending | +| 7.1 | Extend the plan read set in retro and code-review: `prepare`'s full-plan read, the lesson-check partial's "frontmatter, milestones, tasks, DoDs, Verify" instruction, and the review pass's DoD-checkbox cross-reference all name `index.md` plus `milestones/*.md`. | `playbooks/retro/prepare/base.md`, `src/templates/_partials/_plan_lesson_check.j2`, `playbooks/code-review/review/opus-5.md` | 1 | done | +| 7.2 | Update the public docs and the repo guide: plan shape in the vault doc, develop's resume description, groom's plan description, README's plan-track narrative, and the CLAUDE.md Lifecycle bullet that defines a plan directory. | `documentation/vault.md`, `documentation/develop.md`, `documentation/groom.md`, `README.md`, `CLAUDE.md` | 2 | done | #### Task 7.1 DoD -- [ ] No surface still assumes milestone bodies live in `index.md`. -- [ ] Each read set is stated once, as a path pair, with no restated milestone anatomy. +- [x] No surface still assumes milestone bodies live in `index.md`. +- [x] Each read set is stated once, as a path pair, with no restated milestone anatomy. #### Task 7.2 DoD -- [ ] `documentation/vault.md` describes the plan directory including `milestones/`. -- [ ] `documentation/develop.md`'s resume prose points at milestone status, not checkbox scanning in the index. -- [ ] CLAUDE.md's Lifecycle and Config sections name the milestone file contract and `core.plans.milestones`. -- [ ] No stale reference to the single-file plan survives in `README.md`. +- [x] `documentation/vault.md` describes the plan directory including `milestones/`. +- [x] `documentation/develop.md`'s resume prose points at milestone status, not checkbox scanning in the index. +- [x] CLAUDE.md's Lifecycle and Config sections name the milestone file contract and `core.plans.milestones`. +- [x] No stale reference to the single-file plan survives in `README.md`. --- From 65f051ea7090ecdcd1e4d66a126ff4611229e95d Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 19:22:20 +0700 Subject: [PATCH 37/44] test(booping): fixture plans carry milestone files Each hermetic fixture plan gains a milestones/ directory with two files and an index table generated by refresh-milestone-table; cache-warmup keeps none so the missing-sp branch stays covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../plans/19700101-widget-search/index.md | 8 ++++++ .../milestones/01-keyword-index.md | 27 +++++++++++++++++++ .../milestones/02-search-endpoint.md | 27 +++++++++++++++++++ .../plans/19700102-login-timeout/index.md | 8 ++++++ .../milestones/01-reproduce-expiry.md | 27 +++++++++++++++++++ .../milestones/02-fix-idle-window.md | 27 +++++++++++++++++++ .../plans/19700103-session-cleanup/index.md | 8 ++++++ .../milestones/01-orphan-sweep.md | 27 +++++++++++++++++++ .../milestones/02-backfill-existing.md | 27 +++++++++++++++++++ .../plans/19700104-cache-warmup/index.md | 2 +- .../plans/19700105-nav-redesign/index.md | 8 ++++++ .../milestones/01-nav-inventory.md | 27 +++++++++++++++++++ .../milestones/02-single-row-layout.md | 27 +++++++++++++++++++ .../plans/19700106-api-throttling/index.md | 8 ++++++ .../milestones/01-rate-limit-store.md | 27 +++++++++++++++++++ .../milestones/02-throttle-middleware.md | 27 +++++++++++++++++++ 16 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md create mode 100644 playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md create mode 100644 playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md create mode 100644 playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md create mode 100644 playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md create mode 100644 playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md create mode 100644 playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md create mode 100644 playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md create mode 100644 playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md create mode 100644 playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md diff --git a/playbooks/_fixtures/vault/plans/19700101-widget-search/index.md b/playbooks/_fixtures/vault/plans/19700101-widget-search/index.md index f14b6409..de95d195 100644 --- a/playbooks/_fixtures/vault/plans/19700101-widget-search/index.md +++ b/playbooks/_fixtures/vault/plans/19700101-widget-search/index.md @@ -17,3 +17,11 @@ commit: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # Widget search Fixture plan — `done` with a null `retro`, so the retro playbook renders a populated candidate table. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 01 | Keyword index | 3 | done | +| 02 | Search endpoint | 2 | done | + diff --git a/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md new file mode 100644 index 00000000..a5680422 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md @@ -0,0 +1,27 @@ +--- +id: "01" +title: "Keyword index" +sp: 3 +status: done +plan: "plans/19700101-widget-search/index.md" +--- + +# M01: Keyword index + +**Goal**: Every widget write lands in a full-text index. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Index the catalog on write | `src/widgets/index.py` | 3 | done | + +## Definition of Done + +### Task 1.1 + +- [x] A written widget is retrievable by any word in its name. + +## Verify + +`pytest tests/widgets/index_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md new file mode 100644 index 00000000..7c362659 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md @@ -0,0 +1,27 @@ +--- +id: "02" +title: "Search endpoint" +sp: 2 +status: done +plan: "plans/19700101-widget-search/index.md" +--- + +# M02: Search endpoint + +**Goal**: A keyword query returns matching widgets over HTTP. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Serve `GET /widgets?q=` off the index | `src/widgets/api.py` | 2 | done | + +## Definition of Done + +### Task 2.1 + +- [x] A query with no matches returns an empty list, not a 404. + +## Verify + +`pytest tests/widgets/api_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700102-login-timeout/index.md b/playbooks/_fixtures/vault/plans/19700102-login-timeout/index.md index 303a9f3a..27b76480 100644 --- a/playbooks/_fixtures/vault/plans/19700102-login-timeout/index.md +++ b/playbooks/_fixtures/vault/plans/19700102-login-timeout/index.md @@ -17,3 +17,11 @@ commit: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # Login timeout fix Fixture plan — directory shape (`index.md`), `done` with a null `retro`, so the retro playbook renders a populated candidate table. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 01 | Reproduce the early expiry | 1 | done | +| 02 | Fix the idle window | 1 | done | + diff --git a/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md new file mode 100644 index 00000000..40a73faf --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md @@ -0,0 +1,27 @@ +--- +id: "01" +title: "Reproduce the early expiry" +sp: 1 +status: done +plan: "plans/19700102-login-timeout/index.md" +--- + +# M01: Reproduce the early expiry + +**Goal**: A failing test pins the session expiring one minute early. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Regression test at the documented idle window | `tests/auth/session_test.py` | 1 | done | + +## Definition of Done + +### Task 1.1 + +- [x] The test fails on the unfixed code and names the observed drift. + +## Verify + +`pytest tests/auth/session_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md new file mode 100644 index 00000000..595bdd28 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md @@ -0,0 +1,27 @@ +--- +id: "02" +title: "Fix the idle window" +sp: 1 +status: done +plan: "plans/19700102-login-timeout/index.md" +--- + +# M02: Fix the idle window + +**Goal**: Sessions survive the full documented idle window. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Compare against the window's end, not its last tick | `src/auth/session.py` | 1 | done | + +## Definition of Done + +### Task 2.1 + +- [x] The regression test from M01 passes. + +## Verify + +`pytest tests/auth/session_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/index.md b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/index.md index 7f2b47bb..914d26ff 100644 --- a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/index.md +++ b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/index.md @@ -18,3 +18,11 @@ commit: cccccccccccccccccccccccccccccccccccccccc # Session cleanup sweep Fixture plan — directory shape, already covered by a retrospective, so the retro queue skips it and the learn queue has a retrospective to pick up. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 01 | Orphan sweep on expiry | 2 | done | +| 02 | Backfill existing orphans | 1 | done | + diff --git a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md new file mode 100644 index 00000000..38b9b87e --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md @@ -0,0 +1,27 @@ +--- +id: "01" +title: "Orphan sweep on expiry" +sp: 2 +status: done +plan: "plans/19700103-session-cleanup/index.md" +--- + +# M01: Orphan sweep on expiry + +**Goal**: Expiring a session deletes the rows that hang off it. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Cascade the delete from the session row | `src/auth/cleanup.py` | 2 | done | + +## Definition of Done + +### Task 1.1 + +- [x] No orphaned rows remain after an expiry. + +## Verify + +`pytest tests/auth/cleanup_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md new file mode 100644 index 00000000..72b4e641 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md @@ -0,0 +1,27 @@ +--- +id: "02" +title: "Backfill existing orphans" +sp: 1 +status: done +plan: "plans/19700103-session-cleanup/index.md" +--- + +# M02: Backfill existing orphans + +**Goal**: Rows orphaned before the sweep landed are cleared once. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | One-shot backfill command | `src/auth/backfill.py` | 1 | done | + +## Definition of Done + +### Task 2.1 + +- [x] Re-running the command is a no-op. + +## Verify + +`pytest tests/auth/backfill_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700104-cache-warmup/index.md b/playbooks/_fixtures/vault/plans/19700104-cache-warmup/index.md index 177bf410..cbd03516 100644 --- a/playbooks/_fixtures/vault/plans/19700104-cache-warmup/index.md +++ b/playbooks/_fixtures/vault/plans/19700104-cache-warmup/index.md @@ -16,4 +16,4 @@ commit: null # Cache warm-up on deploy -Fixture plan — a null `sp` and a null `created`, so every renderer's missing-value branch is exercised. +Fixture plan — no `milestones/` yet, hence a null `sp` (it is summed from milestone files) and a null `created`, so every renderer's missing-value branch is exercised. diff --git a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/index.md b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/index.md index 9b485555..6cec916a 100644 --- a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/index.md +++ b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/index.md @@ -17,3 +17,11 @@ commit: dddddddddddddddddddddddddddddddddddddddd # Navigation redesign Fixture plan — directory shape, parked at `awaiting-approval` so the specification-phase filters have something to exclude. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 01 | Navigation inventory | 5 | pending | +| 02 | Single-row layout | 3 | pending | + diff --git a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md new file mode 100644 index 00000000..f6923f64 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md @@ -0,0 +1,27 @@ +--- +id: "01" +title: "Navigation inventory" +sp: 5 +status: pending +plan: "plans/19700105-nav-redesign/index.md" +--- + +# M01: Navigation inventory + +**Goal**: Every navigation entry is listed with the breakpoint it breaks at. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Inventory the entries and measure the overflow | `src/nav/entries.ts` | 5 | pending | + +## Definition of Done + +### Task 1.1 + +- [ ] The inventory names an owner for each entry. + +## Verify + +`npm test -- nav/entries` diff --git a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md new file mode 100644 index 00000000..443c1c89 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md @@ -0,0 +1,27 @@ +--- +id: "02" +title: "Single-row layout" +sp: 3 +status: pending +plan: "plans/19700105-nav-redesign/index.md" +--- + +# M02: Single-row layout + +**Goal**: The primary navigation fits one row at every breakpoint. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Collapse overflow entries behind a menu | `src/nav/Bar.tsx` | 3 | pending | + +## Definition of Done + +### Task 2.1 + +- [ ] No entry wraps to a second row at the narrowest breakpoint. + +## Verify + +`npm test -- nav/bar` diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/index.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/index.md index 56b3464b..a33a57e0 100644 --- a/playbooks/_fixtures/vault/plans/19700106-api-throttling/index.md +++ b/playbooks/_fixtures/vault/plans/19700106-api-throttling/index.md @@ -17,3 +17,11 @@ commit: eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # API throttling Fixture plan — directory shape, parked at `in-progress` so the executing phase is represented. + +## Milestones + +| id | title | sp | status | +| --- | --- | --- | --- | +| 01 | Rate limit store | 8 | done | +| 02 | Throttle middleware | 5 | in-progress | + diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md new file mode 100644 index 00000000..e6688d4e --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md @@ -0,0 +1,27 @@ +--- +id: "01" +title: "Rate limit store" +sp: 8 +status: done +plan: "plans/19700106-api-throttling/index.md" +--- + +# M01: Rate limit store + +**Goal**: Per-client request counts are tracked in a shared window store. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 1.1 | Sliding-window counter keyed by client | `src/api/limits.py` | 8 | done | + +## Definition of Done + +### Task 1.1 + +- [x] Counts expire with their window instead of growing unbounded. + +## Verify + +`pytest tests/api/limits_test.py` diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md new file mode 100644 index 00000000..f3a990f3 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md @@ -0,0 +1,27 @@ +--- +id: "02" +title: "Throttle middleware" +sp: 5 +status: in-progress +plan: "plans/19700106-api-throttling/index.md" +--- + +# M02: Throttle middleware + +**Goal**: A client over its limit gets a 429 instead of starving the rest. + +## Tasks + +| Task | Description | Files | SP | Status | +|------|-------------|-------|----|--------| +| 2.1 | Reject over-limit requests with `Retry-After` | `src/api/middleware.py` | 5 | in-progress | + +## Definition of Done + +### Task 2.1 + +- [ ] An under-limit client is never rejected. + +## Verify + +`pytest tests/api/middleware_test.py` From 93aa09e031c1a80d5d4de95ccbfe3dc75393b421 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 19:22:20 +0700 Subject: [PATCH 38/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index 1e56213e..f0b90b43 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -70,7 +70,7 @@ The milestone file is the only place a milestone's work is written down. `index. | 5 | Milestone state machine and table refresh | 4 | done | | 6 | Develop delegates paths, not bodies | 4 | done | | 7 | Downstream readers and documentation | 3 | done | -| 8 | Reports, structure checks and eval fixtures | 2 | pending | +| 8 | Reports, structure checks and eval fixtures | 2 | done | --- @@ -281,7 +281,7 @@ The milestone file is the only place a milestone's work is written down. `index. --- -### M8: Reports, structure checks and eval fixtures — 2 SP | pending +### M8: Reports, structure checks and eval fixtures — 2 SP | done **Goal**: the committed playbook reports, structure rules and eval fixtures match the new plan shape. @@ -291,18 +291,22 @@ The milestone file is the only place a milestone's work is written down. `index. | Task | Description | Files | SP | Status | |------|-------------|-------|----|--------| -| 8.1 | Update the hermetic render fixture and eval fixtures to the multi-file plan shape, and adjust any `mdcheck` rule that asserts milestone structure inside `index.md`. | `playbooks/_fixtures/vault/`, `playbooks/groom/*/_fixtures/`, `playbooks/develop/*/_fixtures/`, `scripts/mdcheck.py` | 1 | pending | -| 8.2 | Run `just snapshots`, read the diff for `groom` and `develop`, and report it for the user to accept; run `just ci` minus the snapshot-accept step and fix what it surfaces. | `playbooks/groom/_reports/output.md`, `playbooks/develop/_reports/output.md` (read-only) | 1 | pending | +| 8.1 | Update the hermetic render fixture and eval fixtures to the multi-file plan shape, and adjust any `mdcheck` rule that asserts milestone structure inside `index.md`. | `playbooks/_fixtures/vault/`, `playbooks/groom/*/_fixtures/`, `playbooks/develop/*/_fixtures/`, `scripts/mdcheck.py` | 1 | done | +| 8.2 | Run `just snapshots`, read the diff for `groom` and `develop`, and report it for the user to accept; run `just ci` minus the snapshot-accept step and fix what it surfaces. | `playbooks/groom/_reports/output.md`, `playbooks/develop/_reports/output.md` (read-only) | 1 | done | #### Task 8.1 DoD -- [ ] Fixture plans carry `milestones/` with at least two milestone files and a generated index table. -- [ ] `just mdcheck` passes against the rendered reports. +- [x] Fixture plans carry `milestones/` with at least two milestone files and a generated index table. +- [x] `just mdcheck` passes against the rendered reports. #### Task 8.2 DoD -- [ ] The snapshot diff is reported to the user; `just snapshots-accept` is **not** run by the worker or the runner. -- [ ] `just lint`, `just typecheck` and `just pytest` are green. +- [x] The snapshot diff is reported to the user; `just snapshots-accept` is **not** run by the worker or the runner. +- [x] `just lint`, `just typecheck` and `just pytest` are green. + +--- + +**Note**: no `mdcheck` rule asserted milestone structure inside `index.md`, so `scripts/mdcheck.py` needed no change; no eval fixtures exist under `playbooks/groom/*/` or `playbooks/develop/*/`. `19700104-cache-warmup` deliberately keeps no `milestones/`, preserving the renderers' missing-`sp` branch. --- From 40e9c0ae8d965c95a014c345136c27cec71ce12a Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 20:38:28 +0700 Subject: [PATCH 39/44] feat(develop): milestone directories, worker-side verify and commit A milestone becomes a directory whose file is named after it, so a runner-written feedback.md can sit beside the contract and a bare wikilink still resolves. The worker now runs its own Verify and commits; the runner validates the diff, keeps the bookkeeping and records failed attempts in feedback.md, routing fixes to a fresh fallback_agent. playbook-state strips the matched segment cleanly so instances key the same in filename and directory positions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- CLAUDE.md | 4 +- README.md | 4 +- .../src/booping/commands/playbook_state.py | 8 +++- .../_playbooks/sharded/flat-step/prompt.md | 5 +++ .../_playbooks/sharded/intake/prompt.md | 5 +++ .../_playbooks/sharded/nested-step/prompt.md | 5 +++ .../_playbooks/sharded/playbook.md | 8 ++++ .../_playbooks/sharded/playbook.yaml | 42 ++++++++++++++++++ .../tests/commands/playbook_state_test.py | 26 ++++++++++- .../tests/commands/scaffold_test.py | 4 +- .../scripts/refresh_milestone_table_test.py | 11 +++-- docs/plan_templates/backend.md | 2 +- docs/plan_templates/claude_skill.md | 2 +- docs/plan_templates/cli.md | 2 +- docs/plan_templates/documentation.md | 2 +- docs/plan_templates/frontend.md | 2 +- documentation/develop.md | 15 ++++--- documentation/groom.md | 8 ++-- documentation/vault.md | 19 +++++++- .../M01-keyword-index.md} | 0 .../M02-search-endpoint.md} | 0 .../M01-reproduce-expiry.md} | 0 .../M02-fix-idle-window.md} | 0 .../M01-orphan-sweep.md} | 0 .../M02-backfill-existing.md} | 0 .../M01-nav-inventory.md} | 0 .../M02-single-row-layout.md} | 0 .../M01-rate-limit-store.md} | 0 .../M02-throttle-middleware.md} | 0 .../M02-throttle-middleware/feedback.md | 5 +++ playbooks/_partials/plan_templates.md | 2 +- playbooks/develop/develop-loop/base.md | 41 ++++++++++-------- playbooks/develop/playbook.yaml | 4 +- playbooks/develop/provision/base.md | 10 ++--- playbooks/groom/draft-plan/opus-5.md | 4 +- playbooks/retro/prepare/base.md | 2 +- src/config.yaml | 43 +++++++++++-------- src/templates/_partials/_developer_body.j2 | 13 ++++-- 38 files changed, 217 insertions(+), 81 deletions(-) create mode 100644 booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/flat-step/prompt.md create mode 100644 booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/intake/prompt.md create mode 100644 booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/nested-step/prompt.md create mode 100644 booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.md create mode 100644 booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.yaml rename playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/{01-keyword-index.md => M01-keyword-index/M01-keyword-index.md} (100%) rename playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/{02-search-endpoint.md => M02-search-endpoint/M02-search-endpoint.md} (100%) rename playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/{01-reproduce-expiry.md => M01-reproduce-expiry/M01-reproduce-expiry.md} (100%) rename playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/{02-fix-idle-window.md => M02-fix-idle-window/M02-fix-idle-window.md} (100%) rename playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/{01-orphan-sweep.md => M01-orphan-sweep/M01-orphan-sweep.md} (100%) rename playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/{02-backfill-existing.md => M02-backfill-existing/M02-backfill-existing.md} (100%) rename playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/{01-nav-inventory.md => M01-nav-inventory/M01-nav-inventory.md} (100%) rename playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/{02-single-row-layout.md => M02-single-row-layout/M02-single-row-layout.md} (100%) rename playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/{01-rate-limit-store.md => M01-rate-limit-store/M01-rate-limit-store.md} (100%) rename playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/{02-throttle-middleware.md => M02-throttle-middleware/M02-throttle-middleware.md} (100%) create mode 100644 playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/feedback.md diff --git a/CLAUDE.md b/CLAUDE.md index c5f65de3..72f084b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ One shipped skill (`/playbook`); everything procedural is a **playbook** it driv - Three-tier deep merge, **core → global → project**: `src/config.yaml` ← `${XDG_CONFIG_HOME:-~/.config}/booping/config.yaml` ← `{vault}/config.yaml`. Later tiers win; lists replace wholesale. No schema gate and no tier restriction — any key loads in any tier. - Top level is exactly `home_dir` + `core`. **Placement rule** under `core`: a key one playbook owns lives at `core.{name}_playbook`; a shared key sits directly under `core`. A user's own playbook namespace copies this shape. - Any mapping in the merged config can be a **query spec** (`booping query --config <dotted.path>`, `| query` filter) or a **scaffold tree** (`booping scaffold <dotted.path> <dest>`) — the value at the dotted path is the spec/tree, no wrapper key. Specs live beside their consumer (`core.{name}_playbook.queries.<id>`), never in a central registry. -- `core.plans` — plan shape shared by groom and develop: `glob` (plan discovery) and `milestones` (`glob` relative to the plan dir + `table_columns` for the generated table). Never restate either literal in prose. +- `core.plans` — plan shape shared by groom and develop: `glob` (plan discovery) and `milestones` (`glob` relative to the plan dir, matching the milestone file inside each milestone directory and never a sidecar beside it, + `table_columns` for the generated table). Never restate either literal in prose. - `core.macros.<name>` — an argv list, or a mapping with `command:` plus `cwd: repo|vault` scoping; rendered bodies call them via the `macro()` global; `--stub-macro` (or a vault `macro_stubs:` mapping) pins them for reproducible renders. - Full reference: [documentation/project_config.md](documentation/project_config.md). @@ -62,7 +62,7 @@ Multi-step guided procedures discovered from three roots — core `playbooks/`, Each playbook owns its own status vocabulary in its `states:` block — there is no shared lifecycle, and those blocks are the authoritative sets. - **Plan track** (`groom` + `develop`): the plan is a directory `{vault}/plans/{slug}/` whose `index.md` is both run artifact and plan document; groom ends at `ready-for-dev`, develop at `done` | `fail`, both also `cancelled`. Shape is the `core.plans.glob` config key (`plans/*/index.md`). -- **Milestone files**: each milestone is a file under the plan directory (`core.plans.milestones.glob`, `milestones/*.md`), frontmatter `id`/`title`/`sp`/`status`/`plan` plus tasks, DoD and Verify. It is the contract handed to a worker by path — `index.md` is context only, its `## Milestones` table (`core.plans.milestones.table_columns`) and the plan's `sp` are generated. Milestone `status:` is develop's per-instance run state (`pending` → `in-progress` → `done`, `blocked` off in-progress); every edge runs the `refresh-milestone-table` hook. +- **Milestone directories**: each milestone is a directory `milestones/M{nn}-{kebab}/` under the plan directory, holding the milestone file named after it (`core.plans.milestones.glob`, `milestones/*/M*.md`) — frontmatter `id`/`title`/`sp`/`status`/`plan` plus tasks, DoD and Verify — and any sidecar the sprint writes beside it, currently `feedback.md`, the runner's findings on a rejected attempt and the only place attempts are counted. The milestone file is the contract handed to a worker by path — `index.md` is context only, its `## Milestones` table (`core.plans.milestones.table_columns`) and the plan's `sp` are generated. Milestone `status:` is develop's per-instance run state (`pending` → `in-progress` → `done`, `blocked` off in-progress), instance `M{nn}-{kebab}`; every edge runs the `refresh-milestone-table` hook. The worker runs its milestone's `## Verify` and makes the repo commit; the runner validates that commit against the DoD, flips the checkboxes and takes the transitions. - **Retro track** (`retro` + `learn`): artifact is a standalone `{vault}/retrospectives/{slug}.md` (`awaiting-retro` → `awaiting-learning` → `done`); plans stay at `done` throughout. Addressed with `--target` since the machines declare no `artifact:`. - **Code-review track** (`code-review`): artifact is `{vault}/codereviews/{plan-dirname}/{ts}.md` (ad-hoc scopes: `codereviews/{target-slug}/{ts}.md`, `plan: null`), machine `in-agent-review` → `human-review` → `done`; plans stay at `done`. Addressed with `--target` since the machine declares no `artifact:`. - The tracks join through **plan frontmatter, not status**: `retro:` is null until covered, so `{status: done, retro: null}` is the retro queue; `code_reviews:` is a list of every review that closed on the plan — history, not a queue flag — so the review queue is every `{status: done}` plan, re-review included. diff --git a/README.md b/README.md index d1da1193..6db01e33 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Candidates are listed for you if you forget the exact path. `groom` shapes the spec and waits for explicit user approval before handing off; `develop` claims the next ready plan and executes milestone by milestone; `retro` compares what shipped to the original spec; `learn` distils the retrospective into rules that bind the next sprint. -A plan is a directory, not a file: `plans/{slug}/index.md` carries the frontmatter, the approach and a generated `## Milestones` table, while each milestone is its own file under `milestones/` holding that milestone's tasks, definition of done and verification. That milestone file is what develop hands a coding agent as its contract, with `index.md` alongside as context. +A plan is a directory, not a file: `plans/{slug}/index.md` carries the frontmatter, the approach and a generated `## Milestones` table, while each milestone gets a directory of its own under `milestones/`. Inside it sits the milestone file — same name as the directory — holding that milestone's tasks, definition of done and verification, joined during the sprint by whatever that milestone accumulates, currently a `feedback.md` written when an attempt is sent back. That milestone file is what develop hands a coding agent as its contract, with `index.md` alongside as context; the agent implements it, runs its verification and commits, and develop validates the commit against the definition of done before moving the milestone on. There is no shared status table — **each playbook has its own status vocabulary** and advances its own run artifact through it. Groom and develop run on the plan, so its `status:` frontmatter is whatever those two last wrote, and the plan track ends when develop closes it — at `done`, or at `fail` or `cancelled`. Retro and learn are a **separate track** over a standalone retrospective under `retrospectives/`; code review is a third, over one file per run under `codereviews/`. The statuses those tracks write are their own artifact's, never the plan's. @@ -117,7 +117,7 @@ A plan's frontmatter status is written by groom or develop. Retro and learn writ - **`framing`** — clarifying the request and settling scope. - **`researching`** — the blast-radius and web-research passes are running. -- **`drafting`** — design is being settled with you in conversation and written into `index.md` and one file per milestone. +- **`drafting`** — design is being settled with you in conversation and written into `index.md` and one milestone directory per milestone. - **`cross-reviewing`** — a second model is reviewing the draft (skipped when no reviewer is configured). - **`presenting`** — the approval screen is on the table. - **`awaiting-approval`** — waiting for your explicit approval or change request; groom's single review gate. diff --git a/booping-python/src/booping/commands/playbook_state.py b/booping-python/src/booping/commands/playbook_state.py index d44ffecf..225e356e 100644 --- a/booping-python/src/booping/commands/playbook_state.py +++ b/booping-python/src/booping/commands/playbook_state.py @@ -103,12 +103,16 @@ def _report_status(artifact: Path, machine: StateMachine) -> dict[str, Any]: def _instances(machine: StateMachine, workdir: Path) -> dict[str, Any]: """Enumerate on-disk instances of a `{instance}` artifact path, keyed by slug — - the path component the placeholder occupies, sorted.""" + what the placeholder itself matched, sorted. The segment carrying it may add a + prefix or suffix (`{instance}.md`); both are stripped off the matched segment.""" parts = machine.artifact.split("/") slug_index = next(i for i, part in enumerate(parts) if "{instance}" in part) + prefix, _, suffix = parts[slug_index].partition("{instance}") found: dict[str, Path] = {} for match in workdir.glob(machine.artifact.replace("{instance}", "*")): - found[match.relative_to(workdir).parts[slug_index]] = match + segment = match.relative_to(workdir).parts[slug_index] + slug = segment[len(prefix) :] + found[slug[: -len(suffix)] if suffix else slug] = match return { slug: _report_status(found[slug], machine) for slug in sorted(found) } diff --git a/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/flat-step/prompt.md b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/flat-step/prompt.md new file mode 100644 index 00000000..7cd351e4 --- /dev/null +++ b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/flat-step/prompt.md @@ -0,0 +1,5 @@ +--- +summary: flat-step body. +--- + +flat-step body. diff --git a/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/intake/prompt.md b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/intake/prompt.md new file mode 100644 index 00000000..3f4c73e1 --- /dev/null +++ b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/intake/prompt.md @@ -0,0 +1,5 @@ +--- +summary: intake body. +--- + +intake body. diff --git a/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/nested-step/prompt.md b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/nested-step/prompt.md new file mode 100644 index 00000000..d843fade --- /dev/null +++ b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/nested-step/prompt.md @@ -0,0 +1,5 @@ +--- +summary: nested-step body. +--- + +nested-step body. diff --git a/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.md b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.md new file mode 100644 index 00000000..7bbbee84 --- /dev/null +++ b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.md @@ -0,0 +1,8 @@ +--- +name: sharded +title: Sharded Playbook +summary: States-bearing fixture exercising {instance} in filename and directory position. +trigger: when a per-instance enumeration test runs +--- + +Sharded preamble. diff --git a/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.yaml b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.yaml new file mode 100644 index 00000000..519cae32 --- /dev/null +++ b/booping-python/tests/__fixtures__/playbook-transition-home/_playbooks/sharded/playbook.yaml @@ -0,0 +1,42 @@ +state: main +graph: + intake: [] + flat-pipeline: + dependencies: [intake] + state: flat + graph: + flat-step: [] + nested-pipeline: + dependencies: [flat-pipeline] + state: nested + graph: + nested-step: [] + +states: + main: + artifact: index.md + initial: intaking + statuses: + intaking: + transitions: + - to: done + when: intake complete + done: {terminal: true} + flat: + artifact: notes/{instance}.md + initial: drafting + statuses: + drafting: + transitions: + - to: done + when: note written + done: {terminal: true} + nested: + artifact: parts/{instance}/{instance}.md + initial: drafting + statuses: + drafting: + transitions: + - to: done + when: part written + done: {terminal: true} diff --git a/booping-python/tests/commands/playbook_state_test.py b/booping-python/tests/commands/playbook_state_test.py index 8d6db581..40aba2bd 100644 --- a/booping-python/tests/commands/playbook_state_test.py +++ b/booping-python/tests/commands/playbook_state_test.py @@ -40,10 +40,11 @@ def _move( state: str | None = None, instance: str | None = None, workdir: Path, + playbook: str = "runner", ) -> None: transition_cmd._run( # type: ignore[reportPrivateUsage] argparse.Namespace( - playbook="runner", + playbook=playbook, to_status=to, state=state, instance=instance, @@ -151,6 +152,29 @@ def test_instances_keyed_by_slug_and_sorted( assert instances["alpha"]["next"] == [{"to": "done", "when": "spec written"}] +@pytest.mark.parametrize( + ("state", "artifact"), + [ + ("flat", "notes/alpha.md"), + ("nested", "parts/alpha/alpha.md"), + ], +) +def test_instance_key_is_what_the_placeholder_matched( + state: str, + artifact: str, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + _plant("sharded") + _move("drafting", state=state, instance="alpha", workdir=tmp_path, playbook="sharded") + capsys.readouterr() + + assert (tmp_path / artifact).is_file() + instances = _report(tmp_path, capsys, playbook="sharded")["states"][state]["instances"] + assert list(instances) == ["alpha"] + assert instances["alpha"]["status"] == "drafting" + + # --------------------------------------------------------------------------- # Failure modes # --------------------------------------------------------------------------- diff --git a/booping-python/tests/commands/scaffold_test.py b/booping-python/tests/commands/scaffold_test.py index a0caefa2..17ff3f12 100644 --- a/booping-python/tests/commands/scaffold_test.py +++ b/booping-python/tests/commands/scaffold_test.py @@ -604,7 +604,7 @@ def test_core_milestone_scaffold_seeds_the_milestone_contract(tmp_path: Path) -> ) assert result.returncode == 0, result.stderr - text = (plan / "milestones" / "01-cli-surface.md").read_text() + text = (plan / "milestones" / "M01-cli-surface" / "M01-cli-surface.md").read_text() front, body = text.split("---\n", 2)[1:] assert list(yaml.safe_load(front).items()) == [ ("id", "01"), @@ -644,7 +644,7 @@ def test_scaffolded_milestones_query_by_the_shared_key(tmp_path: Path) -> None: assert result.returncode == 0, result.stderr result = _run( - "query", "--project", str(vault), "--glob", "plans/demo/milestones/*.md", + "query", "--project", str(vault), "--glob", "plans/demo/milestones/*/M*.md", "--columns", "id,title,sp,status", "--sort", "id", "--output", "json", cwd=tmp_path, ) diff --git a/booping-python/tests/scripts/refresh_milestone_table_test.py b/booping-python/tests/scripts/refresh_milestone_table_test.py index edc1034e..b63f72cb 100644 --- a/booping-python/tests/scripts/refresh_milestone_table_test.py +++ b/booping-python/tests/scripts/refresh_milestone_table_test.py @@ -45,8 +45,8 @@ """ MILESTONES = { - "01-first-thing.md": ('"01"', "First thing", 3, "pending"), - "02-second.md": ('"02"', "Second", 2, "done"), + "M01-first-thing": ('"01"', "First thing", 3, "pending"), + "M02-second": ('"02"', "Second", 2, "done"), } @@ -61,10 +61,13 @@ def _project(tmp_path: Path, *, config: str = "", milestone_dir: str = "mileston (plan / milestone_dir).mkdir(parents=True) (plan / "index.md").write_text(INDEX) for name, (ident, title, sp, status) in MILESTONES.items(): - (plan / milestone_dir / name).write_text( + milestone = plan / milestone_dir / name + milestone.mkdir() + (milestone / f"{name}.md").write_text( f"---\nid: {ident}\ntitle: {title}\nsp: {sp}\nstatus: {status}\n" f"plan: plans/demo/index.md\n---\n\n# {title}\n" ) + (milestone / "feedback.md").write_text("---\nsp: 100\n---\n\nsidecar\n") return plan @@ -137,7 +140,7 @@ def test_follows_the_glob_and_columns_the_config_declares(tmp_path: Path) -> Non tmp_path, milestone_dir="stages", config="core:\n plans:\n milestones:\n" - " glob: stages/*.md\n table_columns: [title, status]\n", + " glob: stages/*/M*.md\n table_columns: [title, status]\n", ) result = _run(plan, tmp_path) diff --git a/docs/plan_templates/backend.md b/docs/plan_templates/backend.md index c96e54bf..bcbf41fb 100644 --- a/docs/plan_templates/backend.md +++ b/docs/plan_templates/backend.md @@ -28,7 +28,7 @@ Generated table — one row per milestone file, projected with `core.plans.miles ## Milestone files -One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: +One file per milestone directory under the plan directory's `milestones/`, named after that directory and seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: - **Goal** — one sentence directly under the H1: what changes in the system after this milestone. - **Scope** — the modules, endpoints or tables in play and the files this milestone touches, so it executes without reading another milestone. diff --git a/docs/plan_templates/claude_skill.md b/docs/plan_templates/claude_skill.md index 100ad782..b486865a 100644 --- a/docs/plan_templates/claude_skill.md +++ b/docs/plan_templates/claude_skill.md @@ -25,7 +25,7 @@ Generated table — one row per milestone file, projected with `core.plans.miles ## Milestone files -One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: +One file per milestone directory under the plan directory's `milestones/`, named after that directory and seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: - **Goal** — one sentence directly under the H1: the observable change in the rendered skill or shared config. - **Scope** — the templates, partials, config keys and rendered artefacts this milestone touches, and which other skills read the same config. diff --git a/docs/plan_templates/cli.md b/docs/plan_templates/cli.md index b2efacaf..81ab5ec1 100644 --- a/docs/plan_templates/cli.md +++ b/docs/plan_templates/cli.md @@ -25,7 +25,7 @@ Generated table — one row per milestone file, projected with `core.plans.miles ## Milestone files -One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: +One file per milestone directory under the plan directory's `milestones/`, named after that directory and seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: - **Goal** — one sentence directly under the H1: the observable change in CLI behavior. - **Scope** — the subcommands and flags in play, the files this milestone touches, and any caller (skill, script) whose expected output shape it affects. diff --git a/docs/plan_templates/documentation.md b/docs/plan_templates/documentation.md index c96d326f..2660f84a 100644 --- a/docs/plan_templates/documentation.md +++ b/docs/plan_templates/documentation.md @@ -39,7 +39,7 @@ The table below is generated — one row per milestone file, projected with `cor ## Milestone files -One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: +One file per milestone directory under the plan directory's `milestones/`, named after that directory and seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: - **Goal** — one sentence directly under the H1: what page(s) or pipeline component lands. - **Scope** — the pages this milestone writes or touches, their place in the page tree, and the surfaces that link to them. diff --git a/docs/plan_templates/frontend.md b/docs/plan_templates/frontend.md index abe27b1c..4ed579f1 100644 --- a/docs/plan_templates/frontend.md +++ b/docs/plan_templates/frontend.md @@ -25,7 +25,7 @@ Generated table — one row per milestone file, projected with `core.plans.miles ## Milestone files -One file per milestone in the plan directory's `milestones/`, seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: +One file per milestone directory under the plan directory's `milestones/`, named after that directory and seeded by `booping scaffold core.groom_playbook.milestone_scaffold` — the seed owns the file's frontmatter keys and its required headings. Write the body into that skeleton: - **Goal** — one sentence directly under the H1: what changes in the UI after this milestone. - **Scope** — the components, routes and state owners in play and the files this milestone touches, plus where new components mount. diff --git a/documentation/develop.md b/documentation/develop.md index 51fc74cf..398b4eba 100644 --- a/documentation/develop.md +++ b/documentation/develop.md @@ -1,6 +1,6 @@ # develop playbook -Execute a groomed plan's milestones, delegating coding to `booping-developer` and orchestrating verification, commits, and status transitions on the way to `done`. +Execute a groomed plan's milestones, delegating coding to `booping-developer` and orchestrating the checks, the feedback and the status transitions on the way to `done`. Development is a **playbook**, not a skill — driven by [`/playbook`](playbook.md): @@ -10,7 +10,7 @@ Development is a **playbook**, not a skill — driven by [`/playbook`](playbook. ## What it does -The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own — every playbook declares its own. Run state lives in the plan's own `index.md`, and each milestone's `pending → in-progress → done` state in its own file under `milestones/`, so a stopped sprint is **resumable**. +The playbook walks one plan through `awaiting-approval → ready-for-dev → in-progress → done`. Three outcomes are terminal: `done`, `fail` (the abort branch), and `cancelled` — a run you call off cancels formally from any non-terminal status, not by being abandoned. `done` closes the plan **immediately**, with no waiting status after it. Retro runs afterwards as its own track — start it with [`/playbook retro`](retro.md); it, like [code-review](code_review.md), picks the plan up through the `retro:` / `code_reviews:` frontmatter seams, each keeping its own run state in its own artifact, without moving the plan's status again. That vocabulary is develop's own — every playbook declares its own. Run state lives in the plan's own `index.md`, and each milestone's `pending → in-progress → done` state in its own milestone file under `milestones/`, so a stopped sprint is **resumable**. Five steps, in dependency order: @@ -18,11 +18,15 @@ Five steps, in dependency order: |------|--------------| | `intake` | Resolve the plan — one entering at `awaiting-approval` is advanced to `ready-for-dev` without asking, since handing it to develop is the approval — and check the plan against the repo's current shape (drift) | | `provision` | Pick and confirm the sprint branch, then settle the milestone groups the briefings will cover | -| `develop-loop` | Brief `booping-developer` per group — milestone files as the contract, `index.md` as context — verify each milestone's DoD, commit as it goes | +| `develop-loop` | Brief `booping-developer` per group — milestone files as the contract, `index.md` as context — then check each returned commit against the milestone's DoD | | `verify` | Run the project's lint / typecheck / test gates plus the plan's Final Verification | | `wrap-up` | Closing commit, sprint report, transition to `done` | -The runner edits no application code — all coding is delegated; it owns reads/writes against the vault, briefing assembly, verification, and commits. +The runner edits no application code — all coding is delegated; it owns reads/writes against the vault, briefing assembly and the plan's own bookkeeping. + +The split at each milestone is deliberate. The **agent** implements the contract, runs the milestone's own verification command until it is green, and makes the repo commit — one commit per milestone, a fix always a new commit. The **runner** never re-runs that command and never commits repo code: it reads the returned commit's diff against the Definition of Done, ticks the checkboxes the diff earns, takes the milestone's transition, and commits the plan in the vault. Whoever ran the command is the one who saw it run. + +When a diff falls short of the DoD, or the agent reports its verification red, the runner writes what it found into a `feedback.md` beside that milestone file — what it checked, what was wrong, what the next attempt must do — and briefs a **fresh** agent with both paths. That file is also where attempts are counted; after two the blocker is unrecoverable, and the run asks you to approve the abort. A milestone the runner has sent back keeps its feedback on disk, so the record survives the session. Every milestone group runs in **one session**. @@ -35,7 +39,7 @@ Every milestone group runs in **one session**. Bare invocation lists the vault's plans at `ready-for-dev` or `awaiting-approval` and asks you to pick one. Name a plan path to target a specific one. -To resume a sprint already `in-progress`, invoke the playbook against the same plan: `playbook-state` reports the frontier and the run picks up at the first milestone file whose `status:` is not `done`. Each transition also regenerates `index.md`'s `## Milestones` table and re-sums the plan's story points, so the table is a view, never the source of truth. +To resume a sprint already `in-progress`, invoke the playbook against the same plan: `playbook-state` reports the frontier and the run picks up at the first milestone whose `status:` is not `done` — including a blocked one, whose `feedback.md` carries the previous attempt's findings into the next briefing. Each transition also regenerates `index.md`'s `## Milestones` table and re-sums the plan's story points, so the table is a view, never the source of truth. ## Branch @@ -74,6 +78,7 @@ The discipline is the [groom playbook](groom.md)'s for cross-review findings: ev The playbook reads these keys from `src/config.yaml`. See [Project config](project_config.md) for the deep-merge override mechanics; per-project tweaks live in `~/Claude/{project}/config.yaml`. - **`core.sprint.max_milestones_per_agent`** — maximum consecutive milestones grouped into a single `booping-developer` briefing; grouping happens only when the milestones share enough context that one agent in sequence beats a fresh agent per milestone. Default `2`. +- **`core.develop_playbook.fallback_agent`** — the agent a fix briefing is routed to when the milestone was built by some other agent, so an externally built milestone still gets its fixes done in-house. Default `booping:booping-developer`. - **`core.develop_playbook.git.branches`** — list of `{branch, when}` entries that map plan `type` (or freeform descriptors) to a branch prefix; `provision` picks from this list. - **`core.develop_playbook.git.commit_message`** — message format string used for in-sprint commits. Override per-project to enforce a different commit shape. - **`core.develop_playbook.agents`** — the agents the playbook may delegate to, with `good_for` / `bad_for` guidance. `booping-developer` is the implementation channel; `booping-researcher` is reserved for the intake drift spot-check across many plan-named files. diff --git a/documentation/groom.md b/documentation/groom.md index b8598227..e2242bf4 100644 --- a/documentation/groom.md +++ b/documentation/groom.md @@ -1,6 +1,6 @@ # groom playbook -Spec a sprint: take a rough request and produce a reviewable plan under `~/Claude/{project}/plans/{slug}/` — an `index.md` plus one file per milestone under `milestones/`, carrying tasks, story points, and definitions of done. +Spec a sprint: take a rough request and produce a reviewable plan under `~/Claude/{project}/plans/{slug}/` — an `index.md` plus one directory per milestone under `milestones/`, each holding that milestone's tasks, story points, and definition of done. Grooming is a **playbook**, not a skill — it is driven by [`/playbook`](playbook.md): @@ -14,9 +14,9 @@ Run states: `framing` → `researching` → `drafting` → `cross-reviewing` → Almost every step runs in your session — inline, or assisted (heavy reads go to the research agent, which returns a bounded summary). The exception is `cross-review`, which hands the drafted plan to a second-model reviewer and does nothing unless you name one. `present` is the run's single approval gate. -The output is a plan directory `~/Claude/{project}/plans/{slug}/`. Its `index.md` carries the approach, the scope and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads; each milestone is a file of its own under `milestones/`, and that file — not a section of the index — is what develop hands a coding agent as its contract. `index.md`'s `## Milestones` table and the plan's total story points are generated from those files, never hand-kept. Groom creates the directory with one `booping scaffold` call from a config-declared tree, so the fresh `index.md` arrives complete — including a real `commit:` stamped with repo HEAD at creation, and `draft-plan` scaffolds each milestone file the same way. The intake briefing and any web-research notes land beside it. The directory doubles as the run workdir, so a groom run is **resumable**: its run state lives in the same `index.md`. +The output is a plan directory `~/Claude/{project}/plans/{slug}/`. Its `index.md` carries the approach, the scope and the YAML frontmatter the rest of the loop (`develop`, `retro`, `learn`) reads; each milestone gets a directory of its own under `milestones/`, holding a milestone file that repeats the directory's name, and that file — not a section of the index — is what develop hands a coding agent as its contract, with room beside it for what the sprint later writes about that milestone. `index.md`'s `## Milestones` table and the plan's total story points are generated from those files, never hand-kept. Groom creates the directory with one `booping scaffold` call from a config-declared tree, so the fresh `index.md` arrives complete — including a real `commit:` stamped with repo HEAD at creation, and `draft-plan` scaffolds each milestone directory and its file the same way. The intake briefing and any web-research notes land beside it. The directory doubles as the run workdir, so a groom run is **resumable**: its run state lives in the same `index.md`. -A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else, and `core.plans.milestones.glob` resolves the milestone files inside it. A vault still holding flat `plans/{slug}.md` files converts them with `/playbook migrate`. +A plan is always a directory: `core.plans.glob` resolves `plans/*/index.md` and nothing else, and `core.plans.milestones.glob` resolves the milestone file inside each milestone directory. A vault still holding flat `plans/{slug}.md` files converts them with `/playbook migrate`. Six steps, in dependency order: @@ -25,7 +25,7 @@ Six steps, in dependency order: | `intake` | Clarify the request, settle scope, scaffold the plan directory — briefing, identity frontmatter, `commit:` at repo HEAD — with one `booping scaffold` call | | `research-codebase` | Map the blast radius — files, modules, integrations, prior art (assisted: heavy reads go to the research agent) | | `research-web` | Check external practice where the design is uncertain, and verify package versions, image tags, API endpoints and CLI flags against current docs | -| `draft-plan` | Design with you in conversation, then write the plan against a plan template — `index.md` plus one milestone file each | +| `draft-plan` | Design with you in conversation, then write the plan against a plan template — `index.md` plus one milestone directory each | | `cross-review` | Hand the drafted plan to a second-model reviewer for severity findings — skipped unless `core.groom_playbook.cross_review_agent` names one (unset by default) | | `present` | Present approach, milestones and SP totals; the run's single approval gate | diff --git a/documentation/vault.md b/documentation/vault.md index c4afe1b5..c8bb693a 100644 --- a/documentation/vault.md +++ b/documentation/vault.md @@ -6,9 +6,24 @@ Reference for every file and directory inside the vault. For the lifecycle that ## `plans/` -A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md` and a `milestones/` subdirectory, authored by the [groom playbook](groom.md) and executed by the [develop playbook](develop.md). `index.md` carries the frontmatter (status, type, story points, a one-line `summary`), the approach and scope, and a `## Milestones` table; each milestone is a file of its own, `milestones/{nn}-{kebab-title}.md`, with frontmatter `id`, `title`, `sp`, `status`, `plan` and a body of tasks, Definition of Done checkboxes and Verify lines. That file is the contract a `booping-developer` agent is handed — develop passes its path, never its text, with `index.md` alongside as context. +A plan is a directory `{YYYYMMDDHHMM}_{kebab-title}/` holding `index.md` and a `milestones/` subdirectory, authored by the [groom playbook](groom.md) and executed by the [develop playbook](develop.md). `index.md` carries the frontmatter (status, type, story points, a one-line `summary`), the approach and scope, and a `## Milestones` table. + +Each milestone gets a directory of its own, `milestones/M{nn}-{kebab-title}/`, and the milestone file inside repeats that name: + +```text +plans/197001010800_widget-search/ +├── index.md +└── milestones/ + ├── M01-keyword-index/ + │ └── M01-keyword-index.md + └── M02-search-endpoint/ + ├── M02-search-endpoint.md + └── feedback.md +``` + +The milestone file is the contract a `booping-developer` agent is handed — develop passes its path, never its text, with `index.md` alongside as context. It states the goal, the tasks, the Definition of Done and the command that verifies the work. Because the file is named after its directory, a bare wikilink to the milestone resolves to it in Obsidian, and the directory stays free for whatever the sprint accumulates around that milestone. Today that is one sidecar: `feedback.md`, written by develop when it sends an attempt back, carrying what it checked, what was wrong and what the next attempt must do. It is absent on a milestone that landed first time. -Discovery follows the `core.plans.glob` config key — an ordered list of globs, `plans/*/index.md` as shipped; the milestone files follow `core.plans.milestones.glob`, `milestones/*.md` relative to the plan directory. A vault laying plans out differently edits those keys and every consumer follows. +Discovery follows the `core.plans.glob` config key — an ordered list of globs, `plans/*/index.md` as shipped; milestones follow `core.plans.milestones.glob`, which matches only the milestone file inside each milestone directory, never a sidecar beside it. A vault laying plans out differently edits those keys and every consumer follows. A plan's `status:` is **run state**, not a shared lifecycle: `index.md` doubles as the run artifact of whichever playbook operates on it, so the vocabulary is the one that playbook declares in its own `states:` block. Groom ends at `ready-for-dev`; develop claims from there and ends at `done`, at `fail` when a blocker survives two fix attempts, or at `cancelled` when you call the run off. All three are terminal. Only `booping playbook-transition` writes the status, and it refuses any move the machine does not declare. See [Playbooks → Run state](playbook.md#run-state). diff --git a/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/M01-keyword-index/M01-keyword-index.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/01-keyword-index.md rename to playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/M01-keyword-index/M01-keyword-index.md diff --git a/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md b/playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/M02-search-endpoint/M02-search-endpoint.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/02-search-endpoint.md rename to playbooks/_fixtures/vault/plans/19700101-widget-search/milestones/M02-search-endpoint/M02-search-endpoint.md diff --git a/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/M01-reproduce-expiry/M01-reproduce-expiry.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/01-reproduce-expiry.md rename to playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/M01-reproduce-expiry/M01-reproduce-expiry.md diff --git a/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md b/playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/M02-fix-idle-window/M02-fix-idle-window.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/02-fix-idle-window.md rename to playbooks/_fixtures/vault/plans/19700102-login-timeout/milestones/M02-fix-idle-window/M02-fix-idle-window.md diff --git a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/M01-orphan-sweep/M01-orphan-sweep.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/01-orphan-sweep.md rename to playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/M01-orphan-sweep/M01-orphan-sweep.md diff --git a/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md b/playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/M02-backfill-existing/M02-backfill-existing.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/02-backfill-existing.md rename to playbooks/_fixtures/vault/plans/19700103-session-cleanup/milestones/M02-backfill-existing/M02-backfill-existing.md diff --git a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/M01-nav-inventory/M01-nav-inventory.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/01-nav-inventory.md rename to playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/M01-nav-inventory/M01-nav-inventory.md diff --git a/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md b/playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/M02-single-row-layout/M02-single-row-layout.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/02-single-row-layout.md rename to playbooks/_fixtures/vault/plans/19700105-nav-redesign/milestones/M02-single-row-layout/M02-single-row-layout.md diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M01-rate-limit-store/M01-rate-limit-store.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/01-rate-limit-store.md rename to playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M01-rate-limit-store/M01-rate-limit-store.md diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/M02-throttle-middleware.md similarity index 100% rename from playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/02-throttle-middleware.md rename to playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/M02-throttle-middleware.md diff --git a/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/feedback.md b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/feedback.md new file mode 100644 index 00000000..a0918cf7 --- /dev/null +++ b/playbooks/_fixtures/vault/plans/19700106-api-throttling/milestones/M02-throttle-middleware/feedback.md @@ -0,0 +1,5 @@ +**Blocked (1/2)**: `pytest tests/api/middleware_test.py` failed — an under-limit client was rejected once the window rolled over. + +Checked the commit diff against the Definition of Done. The middleware resets its counter on the wrong boundary, so the first request of a new window inherits the previous window's count. + +Next attempt: reset the counter against the window the request falls in, and cover the boundary case in `tests/api/middleware_test.py`. diff --git a/playbooks/_partials/plan_templates.md b/playbooks/_partials/plan_templates.md index 8bac43cd..aee5478f 100644 --- a/playbooks/_partials/plan_templates.md +++ b/playbooks/_partials/plan_templates.md @@ -34,4 +34,4 @@ A plan is two surfaces: the run's `index.md`, already carrying its frontmatter a booping frontmatter-update {plan-dir}/index.md summary="{one line}" ``` -`sp` is not — develop's `refresh-milestone-table` script re-sums it from the milestone files. Never hand-write it. +`sp` is not — never hand-write it. diff --git a/playbooks/develop/develop-loop/base.md b/playbooks/develop/develop-loop/base.md index f43c625d..11f3cc1a 100644 --- a/playbooks/develop/develop-loop/base.md +++ b/playbooks/develop/develop-loop/base.md @@ -1,6 +1,8 @@ # Run the sprint, milestone by milestone -This step runs once per milestone file, in `id` order; the instance's milestone is the one its `--instance` names. Provision's groups decide only how briefings are batched: a group's **first** instance composes and delegates one briefing covering that whole group, and the group's later instances ride that briefing straight to their own close. +This step runs once per milestone, in `id` order; the instance's milestone is the one its `--instance` names. Provision's groups decide only how briefings are batched: a group's **first** instance composes and delegates one briefing covering that whole group, and the group's later instances ride that briefing straight to their own close. + +Each milestone owns a directory `{plan-dir}/milestones/{instance}/` holding its milestone file `{instance}.md` — the contract — and, once the runner has findings for it, `feedback.md`. `{instance}` is `M{nn}-{kebab}`. Milestones run **sequentially** — never two workers on one sprint branch, and never edit application code yourself. Don't use git worktrees. @@ -9,10 +11,10 @@ Provision already fired the `ready-for-dev` → `in-progress` edge. On a resume Milestone transitions are the `## State` section's `milestone` machine, invoked as: ``` -booping playbook-transition develop {to} --state milestone --instance {nn}-{kebab} --workdir {plan-dir} +booping playbook-transition develop {to} --state milestone --instance {instance} --workdir {plan-dir} ``` -`{nn}-{kebab}` is the milestone file's name without `.md`. The edge's hook regenerates `index.md`'s `## Milestones` table and its `sp` — never edit either by hand. +The edge's hook regenerates `index.md`'s `## Milestones` table and its `sp` — never edit either by hand. ## Delegating a group @@ -27,14 +29,15 @@ booping playbook-transition develop {to} --state milestone --instance {nn}-{keba ## Inputs - - Contract — `{plan-dir}/milestones/{nn}-{kebab}.md`: one line per milestone in the group, in order. + - Contract — `{plan-dir}/milestones/{instance}/{instance}.md`: one line per milestone in the group, in order. + - Feedback — `{plan-dir}/milestones/{instance}/feedback.md`: one line per milestone, same order; read it if it exists. - Context — `{plan-dir}/index.md`: scope boundary, architecture and decisions. - Conventions — {the repo's `CLAUDE.md`, plus any convention file a milestone names}. - Drift — {intake's outstanding findings, or `none`}. ## Return - One block per milestone, in the same order: what was done in one paragraph, then the files touched. No diffs, no pasted code, no command logs. + One block per milestone, in the same order: what was done in one paragraph, the files touched, the `## Verify` command with its verdict, and the commit sha. No diffs, no pasted code, no command logs. ``` Paths only: never paste a milestone's goal, tasks, DoD or Verify text into the briefing — the worker reads its contract itself. Briefings carry no lesson paths either; the worker gets its lesson context from its own extension file. @@ -43,34 +46,34 @@ booping playbook-transition develop {to} --state milestone --instance {nn}-{keba ## Closing a milestone -Once the briefing that covers this instance's milestone has come back: +Once the briefing that covers this instance's milestone has come back, the worker has already run the milestone's `## Verify` and committed its work. Never re-run that command, and never commit repo code yourself: -1. Verify the diff against the milestone file's `## Definition of Done`. -2. Run the milestone file's `## Verify` command — the project's own guardrails all wait for `verify` at sprint end. -3. In the milestone file: flip each satisfied DoD checkbox `- [ ]` → `- [x]`, and each finished task row's status. Bookkeeping only — no new tasks, no rewritten ones, and never `status:`, which the machine owns. -4. Take the milestone's closing edge. -5. Commit in the attached repo, one commit per milestone, message format `{{ config.core.develop_playbook.git.commit_message }}`. -6. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. -7. Report to the user in one paragraph — what shipped, anything deferred — before the next milestone starts. +1. Validate the worker's commit diff against the milestone file's `## Definition of Done`. +2. In the milestone file: flip each satisfied DoD checkbox `- [ ]` → `- [x]`, and each finished task row's status. Bookkeeping only — no new tasks, no rewritten ones, and never `status:`, which the machine owns. +3. Take the milestone's closing edge. +4. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. +5. Report to the user in one paragraph — what shipped, anything deferred — before the next milestone starts. ## When a milestone does not close -A failing `Verify` or a wrong diff goes back to the worker as a fix briefing — the same block, the same contract path — and the attempt is recorded under the milestone file's `## Notes`: +A diff that misses the DoD, or a `## Verify` the worker reports red, goes back as a fix briefing. First write your findings into `feedback.md` beside the milestone file — what you checked, what was wrong, what the next attempt must do — headed by the attempt record line: + +**Blocked (n/2)**: {what failed} -**Blocked (1/2)**: `{verify command}` failed on {what failed}; re-briefed the worker to {fix}. +`n` is one more than the number of `**Blocked (` lines already in that file; that file is the only place attempts are counted. -Take the milestone's blocked edge on the record, and the edge back when the next attempt starts. After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve the abort, then take the run machine's `in-progress` → `fail` edge. No scope additions and no runner-authored fix at any point. +Take the milestone's blocked edge on the record, and the edge back when the next attempt starts. Brief a **fresh** agent for the fix — the same block, the same contract path, now with the feedback path — routed to `{{ config.core.develop_playbook.fallback_agent }}` when the milestone was built by some other agent. A fix lands as a new commit, never an amend. After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve the abort, then take the run machine's `in-progress` → `fail` edge. No scope additions and no runner-authored fix at any point. ## Return format ``` ## Changed: -- [UPDATED] plans/{slug}/milestones/{nn}-{kebab}.md — {status before} → {status after}, {n} DoD checkboxes flipped -- repo commit: {the milestone's commit message} +- [UPDATED] plans/{slug}/milestones/{instance}/{instance}.md — {status before} → {status after}, {n} DoD checkboxes flipped ## Notes: - briefing: {the group this milestone's briefing covered, or that it rode an earlier group's briefing} -- verify: {the milestone's Verify verdict, and any fix attempts spent} +- commit: {sha} — {the milestone's commit message} +- verify: {the verdict the worker reported, and any fix attempts spent} ``` diff --git a/playbooks/develop/playbook.yaml b/playbooks/develop/playbook.yaml index 4916472f..ce5f11cd 100644 --- a/playbooks/develop/playbook.yaml +++ b/playbooks/develop/playbook.yaml @@ -78,7 +78,7 @@ states: - cancelled milestone: - artifact: milestones/{instance}.md + artifact: milestones/{instance}/{instance}.md initial: pending statuses: pending: @@ -94,7 +94,7 @@ states: hooks: - "script refresh-milestone-table" - to: blocked - when: "a `**Blocked (n/2)**` line under the milestone file's `## Notes` records a failed attempt" + when: "a failed attempt is recorded in `feedback.md` beside the milestone file" hooks: - "script refresh-milestone-table" blocked: diff --git a/playbooks/develop/provision/base.md b/playbooks/develop/provision/base.md index bca10ad2..657265b0 100644 --- a/playbooks/develop/provision/base.md +++ b/playbooks/develop/provision/base.md @@ -5,7 +5,7 @@ Set the sprint up in one step: a confirmed branch to commit on, and the mileston briefing in `develop-loop` will cover. You get the plan — its type, title and slug — the repo's current branch, and the drift findings -intake raised. The milestones come off disk, one file each. +intake raised. The milestones come off disk, one directory each. ## Branch @@ -34,10 +34,10 @@ keep them one-per-briefing. {%- endif %} The groups are yours to settle — reported in the return, never put to the user for confirmation. -Settle them as a table you keep for the return, milestones named by file so `develop-loop` briefs -paths: +Settle them as a table you keep for the return, milestones named by their directory so +`develop-loop` briefs paths: -| Group | Milestone files | SP | Grouped because | +| Group | Milestone dirs | SP | Grouped because | | --- | --- | --- | --- | Carry intake's outstanding drift alongside them, so `develop-loop` briefs against it. @@ -57,7 +57,7 @@ With the branch created and the groups settled, advance the run per the `## Stat - branch: `{name}` created off the current branch `{base}`, name confirmed by the user - transition: {the transition report verbatim} -- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 `01-{kebab}`+`02-{kebab}`, G2 `03-{kebab}` +- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 `M01-{kebab}`+`M02-{kebab}`, G2 `M03-{kebab}` - drift: {what intake raised and whether anything is outstanding} ``` diff --git a/playbooks/groom/draft-plan/opus-5.md b/playbooks/groom/draft-plan/opus-5.md index 0d749b61..c6a7282e 100644 --- a/playbooks/groom/draft-plan/opus-5.md +++ b/playbooks/groom/draft-plan/opus-5.md @@ -26,13 +26,13 @@ conversation already carries — the blast-radius map and the external ground th Yours to write, here, one milestone at a time — no sub-step, no worker agent, no batched pass. `{plan-dir}` is the preamble's `Plan dir:` line. In execution order, per milestone: -1. Seed the file: +1. Seed the milestone's directory and the file inside it: ``` booping scaffold core.groom_playbook.milestone_scaffold {plan-dir}/milestones --set id={nn} --set slug={kebab} --set title="{title}" --set sp={sp} --set plan={plan-dir}/index.md ``` - `{nn}` is the milestone's position in execution order, zero-padded to two digits; `{kebab}` is its title kebab-cased — the two make the filename. The seed owns everything it writes; never retype it into the body. + `{nn}` is the milestone's position in execution order, zero-padded to two digits; `{kebab}` is its title kebab-cased — the two name the directory `M{nn}-{kebab}` and the file `M{nn}-{kebab}.md` inside it, identically. The seed owns everything it writes; never retype it into the body. 2. Write that milestone's body into the seeded file with a normal file edit, against the chosen template's milestone-file section. Then move to the next milestone. diff --git a/playbooks/retro/prepare/base.md b/playbooks/retro/prepare/base.md index 062d3ee9..d1e1554e 100644 --- a/playbooks/retro/prepare/base.md +++ b/playbooks/retro/prepare/base.md @@ -1,4 +1,4 @@ -Read each plan in the working set in full — `index.md` plus `{{ config.core.plans.milestones.glob }}` beside it — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). +Read each plan in the working set — its `index.md` — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). {{ tools.render('src/templates/_partials/_lessons.j2') }} diff --git a/src/config.yaml b/src/config.yaml index 3e2e4b8a..7874ed43 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -62,13 +62,16 @@ core: # and the engine still honours list order for a vault declaring several. glob: - plans/*/index.md - # A milestone is a file inside its plan directory, seeded by - # `core.groom_playbook.milestone_scaffold`. Shared by groom (writes them, - # renders index.md's table) and develop (groups them, delegates their paths): - # `glob` is relative to the plan directory, `table_columns` is the projection - # index.md's `## Milestones` table is rendered with. + # A milestone is a directory inside its plan's `milestones/`, seeded by + # `core.groom_playbook.milestone_scaffold`, holding the milestone file plus any + # sidecars written during the sprint. The milestone file is named after its own + # directory, so a bare wikilink to the directory name resolves to it and `glob` + # never claims a sidecar. Shared by groom (writes them, renders index.md's + # table) and develop (groups them, delegates their paths): `glob` is relative to + # the plan directory, `table_columns` is the projection index.md's + # `## Milestones` table is rendered with. milestones: - glob: milestones/*.md + glob: milestones/*/M*.md table_columns: [id, title, sp, status] sprint: @@ -135,22 +138,23 @@ core: # ints (`01` -> 1) except where a digit forbids it (`08` stays a string), # which mixes types in one column and scrambles `--sort id`. milestone_scaffold: - "{{ id }}-{{ slug }}.md": | - --- - id: {{ id | tojson }} - title: {{ title | tojson }} - sp: {{ sp }} - status: pending - plan: {{ (plan | default('')) | tojson }} - --- + "M{{ id }}-{{ slug }}": + "M{{ id }}-{{ slug }}.md": | + --- + id: {{ id | tojson }} + title: {{ title | tojson }} + sp: {{ sp }} + status: pending + plan: {{ (plan | default('')) | tojson }} + --- - # M{{ id }}: {{ title }} + # M{{ id }}: {{ title }} - ## Tasks + ## Tasks - ## Definition of Done + ## Definition of Done - ## Verify + ## Verify agents: booping-researcher: internal: true @@ -193,6 +197,9 @@ core: - metrics_tokens_cache_read develop_playbook: + # Agent a fix briefing is routed to when the milestone's own builder is not this + # agent — an externally built milestone still gets its fixes done in-house. + fallback_agent: "booping:booping-developer" git: commit_message: '<agent>: <plan title> <message>' branches: diff --git a/src/templates/_partials/_developer_body.j2 b/src/templates/_partials/_developer_body.j2 index 5da2c2bb..8960cb32 100644 --- a/src/templates/_partials/_developer_body.j2 +++ b/src/templates/_partials/_developer_body.j2 @@ -1,12 +1,14 @@ You're an experienced developer. Implement the milestone(s) in the briefing the orchestrator hands you. -The briefing names paths, never pasted content. Its `## Inputs` block gives you a **Contract** line per milestone — that milestone file is the authoritative statement of goal, tasks, files, definition of done and verification — and one **Context** line, the plan's `index.md`, which fixes the scope boundary and never overrides a contract. A briefing may cover one milestone or a small group of consecutive milestones. +The briefing names paths, never pasted content. Its `## Inputs` block gives you a **Contract** line per milestone — that milestone file is the authoritative statement of goal, tasks, files, definition of done and verification — a **Feedback** line per milestone, present only when an earlier attempt was rejected, carrying the orchestrator's findings on it, and one **Context** line, the plan's `index.md`, which fixes the scope boundary and never overrides a contract. A briefing may cover one milestone or a small group of consecutive milestones. ## Workflow -1. Read every contract file in full, then the parts of the context file its milestones reference. +1. Read every contract file in full, each feedback file that exists, then the parts of the context file its milestones reference. 2. Implement exactly what the contracts specify, milestone by milestone in the order given. No extras. No "while I'm here" refactors. -3. Report back using the format below. The orchestrator runs the verification commands after you report. +3. Run the milestone's own `## Verify` command and get it green before moving on. A command you cannot get green is reported red, not worked around. +4. Commit that milestone in the attached repo — one commit per milestone, message format `{{ config.core.develop_playbook.git.commit_message }}`. Never push, never switch or create a branch, never `--amend`: a fix is a new commit. +5. Report back using the format below. ## Hard rules @@ -22,7 +24,7 @@ The briefing names paths, never pasted content. Its `## Inputs` block gives you ## Report format -Signal completion to the orchestrator with a brief per-milestone summary and the files you changed: +Signal completion to the orchestrator with a brief per-milestone summary, the files you changed, and the evidence it validates against: ~~~markdown ## Milestone {id or title} @@ -31,6 +33,9 @@ Signal completion to the orchestrator with a brief per-milestone summary and the Files touched: - path/to/file - path/to/file + +Verify: `{the contract's Verify command}` — PASS|FAIL — {one line of evidence} +Commit: {sha} ~~~ Repeat the block for each milestone in the briefing, in order. From 716d2fc7aabdcdad10cc9591b20e3cff65db95d8 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 20:38:28 +0700 Subject: [PATCH 40/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20in-progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index f0b90b43..f9060ca9 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -310,6 +310,17 @@ The milestone file is the only place a milestone's work is written down. `index. --- +## Post-plan amendments + +Applied on the user's explicit instruction after M8 closed, waiving the sprint's no-scope-additions rule. Recorded here because they change decisions this plan made. + +- **A milestone is a directory**, `milestones/M{nn}-{kebab}/M{nn}-{kebab}.md`, reversing the Decisions entry that rejected a directory per milestone — the runner-written `feedback.md` sidecar needs a home, and naming the file after its dir keeps a bare `[[wikilink]]` resolvable, which also settles the link-cell question. `core.plans.milestones.glob` is `milestones/*/M*.md`, so sidecars are excluded by construction. +- **Milestone status artifact** is `milestones/{instance}/{instance}.md`. `booping playbook-state` gained a fix: the matched segment's prefix and suffix are stripped, so instances key as `M01-cli-surface` in both filename and directory positions. +- **The worker runs its `## Verify` and makes the repo commit**; the runner validates the commit diff against the DoD, flips bookkeeping and takes transitions, and never re-runs a per-milestone Verify. +- **Failed attempts are recorded in `feedback.md`** beside the milestone, `**Blocked (n/2)**` counted only there — the `## Notes` block the milestone machine originally referenced is gone. +- **`core.develop_playbook.fallback_agent`** names the agent a fix briefing routes to when the milestone's builder is not it. +- **`sp` mechanism prose trimmed** from `plan_templates.md` to the ban alone, and retro's `prepare` reads `index.md` only — the milestone glob stays in the delegated lesson-check brief. + ## Final Verification - [ ] `just ci` green (`lint typecheck pytest snapshots mdcheck`), with the snapshot step's diff reported for user acceptance rather than accepted automatically. From 1b7704f6724e6e9c6aedb7e3d5bd15d6e6cbf70a Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 21:00:48 +0700 Subject: [PATCH 41/44] chore(booping): accept playbook report baselines --- playbooks/develop/_reports/output.md | 141 ++++++++++++++++++--------- playbooks/groom/_reports/output.md | 50 +++++++--- playbooks/retro/_reports/output.md | 4 +- 3 files changed, 137 insertions(+), 58 deletions(-) diff --git a/playbooks/develop/_reports/output.md b/playbooks/develop/_reports/output.md index c575aacf..204be9bf 100644 --- a/playbooks/develop/_reports/output.md +++ b/playbooks/develop/_reports/output.md @@ -45,8 +45,9 @@ Execute the steps in the most effective order considering their dependencies. | --- | --- | --- | --- | | `intake` | — | Adopt the plan the preamble resolved — validate its `status:` against an entry transition, advance it to `ready-for-dev` without asking when it entered at `awaiting-approval`, then check plan validity against the repo's current commit: cheap summary first, the plan-named diff only on the user's word; trivial drift patched in place, non-trivial drift halted back to grooming. | — | | `provision` | `intake` | Set the sprint up — pick the branch from the plan's task type per the branch conventions, propose a kebab-case name and create it off the repo's current branch only after the user confirms; then settle the milestone groups the briefings will cover, within the configured ceiling, and fire the `ready-for-dev` → `in-progress` transition. | The user confirms the branch name before the branch is created — asked through `AskUserQuestion`, never as chat prose; a name the user rewrites is used verbatim, and nothing touches git until the answer arrives. The milestone groups are internal — settled by the step, reported in its return, never put to the user | -| `develop-loop` | `provision` | Run the sprint group by group — one briefing per group to the worker agent, one worker at a time on the sprint branch; on each report verify against the milestone's DoD and its plan-authored Verify, flip the checkboxes, task rows and milestone status, commit per milestone, refresh and commit the vault snapshot, and report what shipped before the next group. | — | -| `verify` | `develop-loop` | Run the project's guardrails over the finished sprint once — tests, lint, typecheck, formatter, whatever else must hold for a PR to open without CI failing — plus the plan's own bookkeeping, every DoD checkbox `[x]` and every milestone `done`, read off disk, and return what passed and what failed; no code-quality judgement, no fixes applied here. | — | +| `milestones` *(subgraph)* | `provision` | once per milestone file in the plan's `milestones/`, in `id` order | — | +| `develop-loop` *(in milestones)* | — | Run the sprint group by group — one briefing per group to the worker agent, one worker at a time on the sprint branch; on each report verify against the milestone's DoD and its plan-authored Verify, flip the checkboxes, task rows and milestone status, commit per milestone, refresh and commit the vault snapshot, and report what shipped before the next group. | — | +| `verify` | `milestones` | Run the project's guardrails over the finished sprint once — tests, lint, typecheck, formatter, whatever else must hold for a PR to open without CI failing — plus the plan's own bookkeeping, every DoD checkbox `[x]` and every milestone `done`, read off disk, and return what passed and what failed; no code-quality judgement, no fixes applied here. | — | | `wrap-up` | `verify` | Close the run — update the documentation the sprint invalidated, make one closing commit, fire the `in-progress` → `done` transition, refresh and commit the vault snapshot, then report the sprint and offer a retro. Guardrail results and completeness arrive as verify's evidence and are never re-established here. | — | ## State @@ -79,6 +80,21 @@ booping playbook-state develop --workdir <run workdir> | `fail` | *(terminal)* | — | — | | `cancelled` | *(terminal)* | — | — | +### State: milestone + +- Referenced by: subgraph `milestones` +- Artifact: `milestones/{instance}/{instance}.md` (relative to the run workdir) +- Initial status: `pending` +- Advance: `booping playbook-transition develop <to> --state milestone --instance <slug> --workdir <run workdir>` + +| Status | To | When | Gates | +| --- | --- | --- | --- | +| `pending` | `in-progress` | the milestone's group is being handed to a worker | — | +| `in-progress` | `done` | the worker reported and every checkbox under the milestone file's `## Definition of Done` is `[x]` | — | +| `in-progress` | `blocked` | a failed attempt is recorded in `feedback.md` beside the milestone file | — | +| `blocked` | `in-progress` | the next attempt on the same milestone starts | — | +| `done` | *(terminal)* | — | — | + ## Step: Intake # Adopt the plan @@ -132,8 +148,8 @@ Commit drift: {plan hash}..{current commit hash} Set the sprint up in one step: a confirmed branch to commit on, and the milestone groups every briefing in `develop-loop` will cover. -You get the plan — its type, title, slug, and its milestones with story points and execution -order — the repo's current branch, and the drift findings intake raised. +You get the plan — its type, title and slug — the repo's current branch, and the drift findings +intake raised. The milestones come off disk, one directory each. ## Branch @@ -162,14 +178,21 @@ Format: `<agent>: <plan title> <message>` ## Milestone groups +Enumerate the milestones in execution order — never from `index.md`'s table, which is derived: + +``` +booping query --glob {plan-dir}/milestones/*/M*.md --columns id,title,sp,status --sort id +``` + Group consecutive milestones into agent briefings: each briefing covers **up to 2 milestone(s)**. Group only when the milestones share enough context that one agent handling them in sequence is cheaper than spinning a fresh agent per milestone. Otherwise keep them one-per-briefing. The groups are yours to settle — reported in the return, never put to the user for confirmation. -Settle them as a table you keep for the return: +Settle them as a table you keep for the return, milestones named by their directory so +`develop-loop` briefs paths: -| Group | Milestones | SP | Grouped because | +| Group | Milestone dirs | SP | Grouped because | | --- | --- | --- | --- | Carry intake's outstanding drift alongside them, so `develop-loop` briefs against it. @@ -189,7 +212,7 @@ With the branch created and the groups settled, advance the run per the `## Stat - branch: `{name}` created off the current branch `{base}`, name confirmed by the user - transition: {the transition report verbatim} -- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 M1+M2, G2 M3, G3 M4+M5 +- groups: {n} briefings over {m} milestones (ceiling {c}) — G1 `M01-{kebab}`+`M02-{kebab}`, G2 `M03-{kebab}` - drift: {what intake raised and whether anything is outstanding} ``` @@ -200,64 +223,92 @@ When a branch for this sprint already existed, the branch note says so instead: - branch: `{name}` already existed and was reused — switched onto it, 2 commits ahead of `{base}` ``` +## Subgraph: milestones + +Instructions: +- After: provision +- Repeat: once per milestone file in the plan's `milestones/`, in `id` order +- Inner waves: 1. `develop-loop` + ## Step: Develop Loop -# Run the sprint, group by group +# Run the sprint, milestone by milestone + +This step runs once per milestone, in `id` order; the instance's milestone is the one its `--instance` names. Provision's groups decide only how briefings are batched: a group's **first** instance composes and delegates one briefing covering that whole group, and the group's later instances ride that briefing straight to their own close. + +Each milestone owns a directory `{plan-dir}/milestones/{instance}/` holding its milestone file `{instance}.md` — the contract — and, once the runner has findings for it, `feedback.md`. `{instance}` is `M{nn}-{kebab}`. + +Milestones run **sequentially** — never two workers on one sprint branch, and never edit application code yourself. Don't use git worktrees. -Milestone groups run **sequentially** — never two workers on one sprint branch, and never edit -application code yourself. Don't use git worktrees. +Provision already fired the `ready-for-dev` → `in-progress` edge. On a resume that still finds the plan at `ready-for-dev`, take that edge before the first delegation; otherwise never touch it. -Provision already fired the `ready-for-dev` → `in-progress` edge. On a resume that still finds the -plan at `ready-for-dev`, take that edge before the first delegation; otherwise never touch it. +Milestone transitions are the `## State` section's `milestone` machine, invoked as: -For each confirmed milestone group, in order: +``` +booping playbook-transition develop {to} --state milestone --instance {instance} --workdir {plan-dir} +``` + +The edge's hook regenerates `index.md`'s `## Milestones` table and its `sp` — never edit either by hand. + +## Delegating a group 1. Open one tracking task for the group. -2. Compose **one** briefing covering every milestone in the group: per-milestone request, related - files, DoD and Verify, plus the project conventions and the plan's scope boundary. Briefings - carry no lesson paths — the worker gets its lesson context from its own extension file. -3. Delegate the briefing to the worker agent named in [Available Agents](#available-agents) — - always delegate, even for a one-line change. -4. Do not continue next milestone in the same agent by resurrecting it with ID. Always start a fresh agent with empty context. -5. When the worker reports done, for **each milestone** in the group: - - Verify the output against the milestone's DoD and the resulting diff. - - Run the milestone's plan-authored `Verify` command — the project's own guardrails all wait for - `verify` at sprint end. - - Flip each completed task's DoD checkboxes in the plan: `- [ ]` → `- [x]`. - - Flip each task row in the milestone's status table: `pending` → `done`. - - Flip the milestone status to `done`. - - Commit in the attached repo, one commit per milestone, message format - `<agent>: <plan title> <message>`. -5. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then - `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. -6. Report group completion to the user with a one-paragraph summary (what shipped, anything - deferred) before starting the next group. - -Plan edits here are bookkeeping only: no new milestones, no rewritten tasks, and never `status:`, -which the run machine owns. +2. Transition every milestone in the group onto the edge whose `when` is the group being handed to a worker. +3. Compose **one** briefing, exactly this block: + + ```markdown + ## Task + + Implement the milestones below, in the order given, on branch `{branch}`. + + ## Inputs + + - Contract — `{plan-dir}/milestones/{instance}/{instance}.md`: one line per milestone in the group, in order. + - Feedback — `{plan-dir}/milestones/{instance}/feedback.md`: one line per milestone, same order; read it if it exists. + - Context — `{plan-dir}/index.md`: scope boundary, architecture and decisions. + - Conventions — {the repo's `CLAUDE.md`, plus any convention file a milestone names}. + - Drift — {intake's outstanding findings, or `none`}. + + ## Return + + One block per milestone, in the same order: what was done in one paragraph, the files touched, the `## Verify` command with its verdict, and the commit sha. No diffs, no pasted code, no command logs. + ``` + + Paths only: never paste a milestone's goal, tasks, DoD or Verify text into the briefing — the worker reads its contract itself. Briefings carry no lesson paths either; the worker gets its lesson context from its own extension file. +4. Delegate the briefing to the worker agent named in [Available Agents](#available-agents) — always delegate, even for a one-line change. +5. Never resurrect a worker by ID for the next group. Each group gets a fresh agent with empty context. + +## Closing a milestone + +Once the briefing that covers this instance's milestone has come back, the worker has already run the milestone's `## Verify` and committed its work. Never re-run that command, and never commit repo code yourself: + +1. Validate the worker's commit diff against the milestone file's `## Definition of Done`. +2. In the milestone file: flip each satisfied DoD checkbox `- [ ]` → `- [x]`, and each finished task row's status. Bookkeeping only — no new tasks, no rewritten ones, and never `status:`, which the machine owns. +3. Take the milestone's closing edge. +4. Commit the plan in the vault git repo: `git -C {vault} add plans/{slug}`, then `git -C {vault} commit -q -m "develop: {slug} → in-progress"`. +5. Report to the user in one paragraph — what shipped, anything deferred — before the next milestone starts. ## When a milestone does not close -A failing `Verify` or a wrong diff goes back to the worker as a fix briefing, and the attempt is -recorded under the milestone in the plan: +A diff that misses the DoD, or a `## Verify` the worker reports red, goes back as a fix briefing. First write your findings into `feedback.md` beside the milestone file — what you checked, what was wrong, what the next attempt must do — headed by the attempt record line: + +**Blocked (n/2)**: {what failed} -**Blocked (1/2)**: `{verify command}` failed on {what failed}; re-briefed the worker to {fix}. +`n` is one more than the number of `**Blocked (` lines already in that file; that file is the only place attempts are counted. -After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve -the abort, then take the `in-progress` → `fail` edge. No scope additions and no runner-authored fix -at any point. +Take the milestone's blocked edge on the record, and the edge back when the next attempt starts. Brief a **fresh** agent for the fix — the same block, the same contract path, now with the feedback path — routed to `booping:booping-developer` when the milestone was built by some other agent. A fix lands as a new commit, never an amend. After two recorded attempts on the same issue the blocker is unrecoverable: ask the user to approve the abort, then take the run machine's `in-progress` → `fail` edge. No scope additions and no runner-authored fix at any point. ## Return format ``` ## Changed: -- [UPDATED] plans/{slug}/index.md — {groups closed, milestones flipped} -- repo commits: {one line per milestone commit} +- [UPDATED] plans/{slug}/milestones/{instance}/{instance}.md — {status before} → {status after}, {n} DoD checkboxes flipped ## Notes: -- {per group: what shipped, anything deferred} -- {the Verify verdict per milestone, and any fix attempts spent} +- briefing: {the group this milestone's briefing covered, or that it rode an earlier group's briefing} +- commit: {sha} — {the milestone's commit message} +- verify: {the verdict the worker reported, and any fix attempts spent} ``` ## Step: Verify diff --git a/playbooks/groom/_reports/output.md b/playbooks/groom/_reports/output.md index ed071485..f20a648c 100644 --- a/playbooks/groom/_reports/output.md +++ b/playbooks/groom/_reports/output.md @@ -33,7 +33,7 @@ Execute the steps in the most effective order considering their dependencies. | `intake` | — | Restate the request, classify the task type, set the scope boundaries and challenge the scope in a brief written to `request.md` and posted in chat; create the plan directory with its `index.md`, or adopt a parked plan into it. | The user answers the scope-challenge questions — clear intent is the confirmation, no separate confirm is asked for; answers that change the task type, the restated problem or a boundary send the step back for another pass | | `research-codebase` | `intake` | Map the blast radius in the attached repo — touched surfaces, prior art, the conventions that bind the design, and the calls left for it; the bulk reads are delegated, the map is posted in chat. | — | | `research-web` | `research-codebase` | Research the external ground the design rests on — current best practice, competing approaches and known pitfalls where the work is uncertain, and the external references it names, each checked against current docs. | — | -| `draft-plan` | `research-codebase`, `research-web` | Settle architecture, surface changes and trade-offs with the user, then pick the plan template matching the dominant surface and write the plan against its Plan Body — milestones, tasks with DoD and Verify, story points per task / milestone / sprint, `sp` and `summary` frontmatter; verify against the template's Quality Checklist before returning. | — | +| `draft-plan` | `research-codebase`, `research-web` | Settle architecture, surface changes and trade-offs with the user, then pick the plan template matching the dominant surface and write the plan against its Plan Body — `index.md` plus one seeded file per milestone with its tasks, DoD, Verify and story points, the index's milestone table generated from those files, and `summary` frontmatter; verify against the template's Quality Checklist before returning. | — | | `cross-review` | `draft-plan` | Second-model review of the written plan — the agent reads the plan file `plans/{slug}/index.md` and returns severity findings only, writing nothing. Dispose of the findings yourself before advancing: `CRITICAL` folded into the plan or recorded as a deferral in `## Risk register`, `RISK` folded in unless it reopens a call the user settled, `NOTE` at your discretion; a finding that reopens a settled design call is folded in nowhere and sends the run back to `drafting`. | — | | `present` | `cross-review` | Assemble the approval summary — approach, milestones, SP totals, plan path and every check outcome; recommend a split when the total passes the threshold, offer a plan branch on a repo-local vault, and carry the approval. | The run's only review gate — the summary and the full plan are approved together, and the plan reaches the `develop` playbook through this gate and no other. Ask for the approval in prose, in the message itself — never via `AskUserQuestion`. Explicit user approval: "looks good" counts, silence never does; on that word the run moves to `ready-for-dev`. A change request loops the run back to the status that owns what it touches: any change to the plan — architecture, scope, milestones, tasks or estimates — sends the run back to `drafting`, and present never absorbs a change itself. A recommended split is acknowledged, not required: the user may approve the plan whole and park no siblings | @@ -145,8 +145,7 @@ conversation already carries — the blast-radius map and the external ground th - **Draft design with the user**: architecture, pattern choices, data / API / config surface changes, open trade-offs. Iterate until aligned before writing. -- **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) - whose name + description matches the work, then produce the plan against its `# Plan Body`. +- **Write the plan**: pick a plan template from [Available plan templates](#available-plan-templates) whose name + description matches the work, then produce the plan against its `# Plan Body` — `index.md` from the template's top-level sections, plus one file per milestone. - **Write `summary`**: set the `summary:` frontmatter to a single line of plain plan intent — ≤ ~120 chars / ~20 words, no prose, no trailing period needed. It feeds search and the `sprints.md` snapshot. @@ -154,7 +153,7 @@ conversation already carries — the blast-radius map and the external ground th ## Hard rules - The orchestrator never edits files outside `{project}/plans/`. -- Each milestone executable in a fresh session with only the plan as context. +- Each milestone file executable in a fresh session with only it and `index.md` as context. - Sprint total past **35 SP** — offer the user a split at a dependency seam (the first slice shippable on its own, each later one useless without it), keep only the first slice that fits the threshold in this plan, and park the rest as sibling stubs to be groomed in their own runs. They @@ -188,13 +187,38 @@ it — frontmatter (`name`, `description`) plus both top-level sections, generic class: placeholders throughout, no path, milestone or story-point value from this run baked in. Never draft into a bad-fit template, never improvise a shape and name a template after it. -The plan is the run's `index.md`, already carrying its frontmatter and title — the body goes under -them. `sp` and `summary` are yours; write them with: +A plan is two surfaces: the run's `index.md`, already carrying its frontmatter and title, holding the template's top-level sections under them; and one file per milestone, written as [Write the milestone files](#write-the-milestone-files) describes, against the template's milestone-file section. + +`summary` is yours: ``` -booping frontmatter-update {plan}/index.md sp={total} summary="{one line}" +booping frontmatter-update {plan-dir}/index.md summary="{one line}" ``` +`sp` is not — never hand-write it. + + +## Write the milestone files + +Yours to write, here, one milestone at a time — no sub-step, no worker agent, no batched pass. `{plan-dir}` is the preamble's `Plan dir:` line. In execution order, per milestone: + +1. Seed the milestone's directory and the file inside it: + + ``` + booping scaffold core.groom_playbook.milestone_scaffold {plan-dir}/milestones --set id={nn} --set slug={kebab} --set title="{title}" --set sp={sp} --set plan={plan-dir}/index.md + ``` + + `{nn}` is the milestone's position in execution order, zero-padded to two digits; `{kebab}` is its title kebab-cased — the two name the directory `M{nn}-{kebab}` and the file `M{nn}-{kebab}.md` inside it, identically. The seed owns everything it writes; never retype it into the body. + +2. Write that milestone's body into the seeded file with a normal file edit, against the chosen template's milestone-file section. Then move to the next milestone. + +`index.md`'s `## Milestones` table is generated from the files on disk, never hand-kept — paste the output of: + +``` +booping query --glob {plan-dir}/milestones/*/M*.md --columns id,title,sp,status +``` + +A milestone that changes after that is edited in its own file, and the query re-run. ## Sprint planning @@ -239,17 +263,21 @@ Second-model review of the written plan — the agent reads the plan file `plans Tell the `codex` agent to get its instructions by calling this command: `booping render-playbook groom --step cross-review`. ## Step: Present +Read the plan's milestone rows — never a hand-kept list — with `{plan-dir}` the preamble's `Plan dir:` line: + +``` +booping query --glob {plan-dir}/milestones/*/M*.md --columns id,title,sp,status +``` + Present user the resulting plan as: ``` Request: {path} Plan: {path} Status: {status} -SPs: {SP total} +SPs: {sum of the rows' sp} -| # | Summary | SP | -| - | ------- | -- | -{one row per milestone — its id, what it delivers, its SP} +{the table the query printed} ## Next Steps diff --git a/playbooks/retro/_reports/output.md b/playbooks/retro/_reports/output.md index eaa790b9..6230f62e 100644 --- a/playbooks/retro/_reports/output.md +++ b/playbooks/retro/_reports/output.md @@ -98,7 +98,7 @@ Resolve `$ARGUMENTS` to plan paths. ``` ## Step: Prepare -Read each plan in the working set in full — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). +Read each plan in the working set — its `index.md` — for **context only**: scope, SP totals, dates, decisions on record. The plan is a reference for understanding issues that surface in the later steps, not a target for orchestrator analysis (no derived "decisions deviated" / "tech debt" / "coverage gap" findings — the user owns issue identification; the step does homework and suggests options). @@ -122,7 +122,7 @@ Drop items with low value for the retrospective. Do not copy raw logs into the s **B. Plan-stage lesson check** — separate brief, run in parallel with A: -1. Read the plan file(s) listed in the brief in full — frontmatter, milestones, tasks, DoDs, Verify lines. +1. Read each plan listed in the brief in full — `index.md` plus `milestones/*/M*.md` beside it. 2. Cross-check the plan against the lesson set included verbatim in the brief. 3. Return a structured summary per plan with one section: - **Plan-stage lesson gaps** — places where the plan as written contradicts or omits a loaded lesson. Per item: lesson path, the rule, where in the plan it should have shown up (milestone / task / DoD / Verify), and what is there instead (or what is missing). From 1b70954b6accd0668108776e5ac10caefb0ebf33 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Mon, 10 Aug 2026 21:01:58 +0700 Subject: [PATCH 42/44] =?UTF-8?q?develop:=20202608101646=5Fmilestone-files?= =?UTF-8?q?-for-dev-agents=20=E2=86=92=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md index f9060ca9..52983517 100644 --- a/vault/plans/202608101646_milestone-files-for-dev-agents/index.md +++ b/vault/plans/202608101646_milestone-files-for-dev-agents/index.md @@ -1,13 +1,13 @@ --- title: "Per-milestone plan files handed straight to dev agents" type: "feature" -status: in-progress +status: done sp: 25 related_to: null created: 2026-08-10 16:47 planned: null started: 2026-08-10 17:36 -completed: null +completed: 2026-08-10 21:01 code_reviews: [] sessions: - 230b182a-514b-434f-926c-e4cb0ddab343 @@ -17,6 +17,13 @@ summary: Plans split into plans/{slug}/milestones/*.md — scaffold-seeded, state-machine status, develop briefs paths not bodies commit: 6da8ccabbf1010dc0d500c10b6620e7f28faf429 reviewed_at: 2026-08-10 17:32 +metrics_active_minutes: 101 +metrics_models: +- claude-opus-5 +metrics_tokens_input: 451 +metrics_tokens_output: 224891 +metrics_tokens_cache_creation: 992523 +metrics_tokens_cache_read: 23914940 --- # Per-milestone plan files handed straight to dev agents From 6d163ea6740ef8de6ae75816fcafdb1668aedd88 Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Tue, 11 Aug 2026 11:30:58 +0700 Subject: [PATCH 43/44] feat(develop): link milestone titles to their files in index.md table Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- playbooks/develop/_scripts/refresh-milestone-table | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/playbooks/develop/_scripts/refresh-milestone-table b/playbooks/develop/_scripts/refresh-milestone-table index b94b6e23..9953952f 100755 --- a/playbooks/develop/_scripts/refresh-milestone-table +++ b/playbooks/develop/_scripts/refresh-milestone-table @@ -52,12 +52,20 @@ def cell(value: Any) -> str: return str(value).replace("|", "\\|").replace("\n", " ") +def render(row: dict[str, Any], col: str) -> str: + # `title` links to the milestone file so the table navigates; `path` is + # plan-dir-relative, which is exactly where index.md lives. + if col == "title" and row.get("path"): + return f"[{cell(row.get(col))}]({row['path']})" + return cell(row.get(col)) + + def table(rows: list[dict[str, Any]], columns: list[str]) -> str: lines = [ "| " + " | ".join(columns) + " |", "| " + " | ".join("---" for _ in columns) + " |", ] - lines += ["| " + " | ".join(cell(row.get(col)) for col in columns) + " |" for row in rows] + lines += ["| " + " | ".join(render(row, col) for col in columns) + " |" for row in rows] return "\n".join(lines) + "\n" From 6f7f781f26ac8002f8d6f6e498453409690f21ac Mon Sep 17 00:00:00 2001 From: Anton Shuvalov <anton@shuvalov.info> Date: Tue, 11 Aug 2026 11:36:17 +0700 Subject: [PATCH 44/44] test(develop): expect linked milestone titles in table Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .../tests/scripts/refresh_milestone_table_test.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/booping-python/tests/scripts/refresh_milestone_table_test.py b/booping-python/tests/scripts/refresh_milestone_table_test.py index b63f72cb..715e9e7d 100644 --- a/booping-python/tests/scripts/refresh_milestone_table_test.py +++ b/booping-python/tests/scripts/refresh_milestone_table_test.py @@ -91,8 +91,8 @@ def test_renders_one_row_per_milestone_file(tmp_path: Path) -> None: assert _section((plan / "index.md").read_text(), "## Milestones") == ( "| id | title | sp | status |\n" "| --- | --- | --- | --- |\n" - "| 01 | First thing | 3 | pending |\n" - "| 02 | Second | 2 | done |" + "| 01 | [First thing](milestones/M01-first-thing/M01-first-thing.md) | 3 | pending |\n" + "| 02 | [Second](milestones/M02-second/M02-second.md) | 2 | done |" ) @@ -121,9 +121,8 @@ def test_writes_the_table_into_a_section_that_has_none(tmp_path: Path) -> None: result = _run(plan, tmp_path) assert result.returncode == 0, result.stderr - assert "| 01 | First thing | 3 | pending |" in _section( - (plan / "index.md").read_text(), "## Milestones" - ) + row = "| 01 | [First thing](milestones/M01-first-thing/M01-first-thing.md) | 3 | pending |" + assert row in _section((plan / "index.md").read_text(), "## Milestones") def test_second_run_changes_nothing(tmp_path: Path) -> None: @@ -148,8 +147,8 @@ def test_follows_the_glob_and_columns_the_config_declares(tmp_path: Path) -> Non assert _section((plan / "index.md").read_text(), "## Milestones") == ( "| title | status |\n" "| --- | --- |\n" - "| First thing | pending |\n" - "| Second | done |" + "| [First thing](stages/M01-first-thing/M01-first-thing.md) | pending |\n" + "| [Second](stages/M02-second/M02-second.md) | done |" )