From ebbb5ccb3b220ffa39c4eae04478dde9b44614ed Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 20 Jun 2026 17:39:54 -0500 Subject: [PATCH] bmm: address PR review on memlog standardization - Delete orphaned bmad-brainstorming/scripts/tests/test_memlog.py: it imported the bundled memlog.py removed in this PR (ModuleNotFoundError on collection) and its unique tests asserted the now-removed status-lifecycle behavior. The canonical src/scripts/tests/test_memlog.py is the corrected superset, so no coverage is lost. - Make runnable memlog command examples self-contained with the full 'uv run {project-root}/_bmad/scripts/memlog.py ... --workspace {doc_workspace}' form across bmad-brainstorming (converge/finalize/headless), bmad-prd, and bmad-ux. Terse checklist back-references left short by design. - bmad-product-brief Update: init .memlog.md if missing (legacy/pre-standard briefs), matching bmad-prd and bmad-ux; fix the --type override invocation. --- .../1-analysis/bmad-product-brief/SKILL.md | 2 +- .../2-plan-workflows/bmad-prd/SKILL.md | 4 +- .../2-plan-workflows/bmad-ux/SKILL.md | 4 +- .../bmad-brainstorming/references/converge.md | 2 +- .../bmad-brainstorming/references/finalize.md | 2 +- .../bmad-brainstorming/references/headless.md | 4 +- .../scripts/tests/test_memlog.py | 265 ------------------ 7 files changed, 9 insertions(+), 274 deletions(-) delete mode 100644 src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py diff --git a/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md b/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md index baa85f9f8..ad40bf72c 100644 --- a/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +++ b/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md @@ -30,7 +30,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen **Create.** A brief the user is proud of, that meets their needs, drawn out through real conversation — do not assume: instead converse and understand, and then help craft the best product brief for their needs. Begin in `## Discovery` before drafting; the brief comes after the picture is on the table. Shape follows the product and need. Treat `{workflow.brief_template}` as a starting structure, not a contract: drop sections that do not earn their place, add sections the product needs, reorder freely - create sections for specialized domains or concerns also as needed. The brief serves the product's story, not the template's shape. Bind `{doc_workspace}` to a fresh folder at `{workflow.brief_output_path}/{workflow.run_folder_pattern}/`, write `brief.md` there with YAML frontmatter (title, status, created, updated), and seed the memlog: `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic=""`. For Update and Validate, `{doc_workspace}` is the existing folder of the brief being targeted. -**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). Surface conflicts with prior decisions before changing. Headless override: log the reversal via `memlog.py append --type override`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching. +**Update.** Reconcile an existing brief with a change signal. Before proposing changes, read the brief, addendum, `.memlog.md`, and original inputs — and run the `## Discovery` posture against the change signal (a patch applied without context becomes drift). If `.memlog.md` is missing (a legacy or pre-standard brief), init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` first — this update is its first entry. Surface conflicts with prior decisions before changing. Headless override: log the reversal via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type override --text ""`, then apply; halt `blocked` if intent is ambiguous. If the change is fundamental, offer Create instead of patching. **Validate.** Honest critique against the brief's own purpose. Read the brief, the addendum if present, `.memlog.md`, and any original inputs first — a validation that ignores prior decisions, rejected ideas, or context the user supplied is shallow. Cite specific lines. Caveat what cannot be evaluated. Return inline — no separate file unless asked. Always offer to roll findings into an Update, even in headless mode — include `"offer_to_update": true` in the JSON status block. diff --git a/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md b/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md index 4ba158911..6ebfeab3f 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +++ b/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md @@ -29,7 +29,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen **Create.** Bind `{doc_workspace}` to `{workflow.prd_output_path}/{workflow.run_folder_pattern}/`. Write `prd.md` with YAML frontmatter (title, status, created, updated — initial `status: draft`), and seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic=""` so subsequent decisions land in a known file. Tell the user the path. Run `## Discovery`, then `## Finalize`. -**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, `memlog.py init` it, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `memlog.py append` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`. +**Update.** Reconcile the PRD with a change signal. Source-extract against PRD, addendum, `.memlog.md`, and original inputs (extract, don't ingest). If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}`, then spawn a one-time bootstrap subagent to reverse-engineer a thin log from the PRD (one `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text ""` per recovered decision) before continuing. Surface conflicts with prior decisions before applying. Then `## Finalize`. **Validate** (or *analyze*). Critique without changing. Load `references/validate.md`. @@ -88,5 +88,5 @@ Tell the user the sequence in one sentence, then walk it. Polish goes last so it 4. **Triage open items.** All Open Questions, `[ASSUMPTION]` tags, `[NOTE FOR PM]` callouts. Phase-blockers (would make the PRD unsafe for UX/architecture/epics) surfaced one at a time and resolved; non-blockers deferred with owner + revisit condition logged via `memlog.py append`. If phase-blocker count is high, flag it. 5. **Polish.** Apply `{workflow.doc_standards}` to `prd.md` and `addendum.md` in declared order (structural passes before prose — prose should not polish soon-to-be-cut text). Parallelize across documents, sequential within. 6. **External handoffs.** Execute `{workflow.external_handoffs}`; surface returned URLs/IDs. Skip and flag unavailable tools. -7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `memlog.py append --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing. +7. **Close.** Set `prd.md` frontmatter `status: final` and `updated` to `{date}` so future invocations distinguish this PRD from in-progress drafts. Record finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "PRD finalized"`. Share artifact paths. Common next: `bmad-ux`, `bmad-architecture`, `bmad-create-epics-and-stories`; invoke `bmad-help` for authoritative routing. 8. Run `{workflow.on_complete}` if non-empty. diff --git a/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md b/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md index 131a6c9ea..b441b2196 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +++ b/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md @@ -44,7 +44,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen **Create.** Bind `{doc_workspace}` to `{workflow.ux_output_path}/{workflow.run_folder_pattern}/`. Create `.working/` and `imports/`; seed the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic=""`; create `DESIGN.md` (frontmatter only) and `EXPERIENCE.md` (frontmatter only). Run Discovery → Finalize. -**Update.** Read spines + memlog + sources. `memlog.py init` the memlog if missing — this update is entry one. Surface conflicts with prior decisions. Run Finalize. +**Update.** Read spines + memlog + sources. If `.memlog.md` is missing, init it with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace}` — this update is entry one. Surface conflicts with prior decisions. Run Finalize. **Validate.** See `references/validate.md`. @@ -87,4 +87,4 @@ Outcomes, in order: - **Key-screen mocks rendered.** Key-screens tool → `.working/` for surfaces where layout drives behavior or anchors visual language. - **Mock coverage confirmed.** Walk every IA surface; classify *mocked* vs *spine-only*. Ask: *"These will be built from spine tables alone — any need a visual reference?"* Render more if named; log spine-only choices. - **Layout extracted, artifacts promoted.** Distill subagent re-reads each `.working/` and `imports/` artifact; lifts visual decisions into DESIGN.md and behavioral decisions into EXPERIENCE.md. Promote `.working/` keepers to `mockups/` (HTML) or `wireframes/` (Excalidraw); imports stay. Inline relative links at relevant spine sections; state spines-win-on-conflict once. -- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `memlog.py append --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`. +- **Polished, handed off, closed.** Apply `{workflow.doc_standards}` in order. Execute `{workflow.external_handoffs}`; surface URLs. Set both files' `status: final`, `updated: {date}`. Log finalization via `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type event --text "spines finalized"`. Share paths. Common next: `bmad-architecture`, `bmad-create-epics-and-stories`, `bmad-dev-story`. Run `{workflow.on_complete}`. diff --git a/src/core-skills/bmad-brainstorming/references/converge.md b/src/core-skills/bmad-brainstorming/references/converge.md index a24231fcc..ac1786e67 100644 --- a/src/core-skills/bmad-brainstorming/references/converge.md +++ b/src/core-skills/bmad-brainstorming/references/converge.md @@ -17,7 +17,7 @@ Pick by what the decision needs: - **PMI (Plus / Minus / Interesting)** — when one strong candidate needs pressure-testing before commitment: list its pluses, minuses, and the merely-interesting, then judge. - **MoSCoW** — when scoping a build: sort into Must / Should / Could / Won't-this-time. -Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --type decision --text ""` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing. +Log the surviving directions and the reasoning with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type decision --text ""` (use `--by` in Creative Partner mode). Two or three convergence moves chained is fine (e.g. cluster → score the clusters); more than that is usually over-processing. ## Then finalize diff --git a/src/core-skills/bmad-brainstorming/references/finalize.md b/src/core-skills/bmad-brainstorming/references/finalize.md index c7a888472..2a4b6d25f 100644 --- a/src/core-skills/bmad-brainstorming/references/finalize.md +++ b/src/core-skills/bmad-brainstorming/references/finalize.md @@ -9,7 +9,7 @@ In Facilitator mode this is the one place your own creative contribution is welc 1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier, not just the recent obvious ones (in Creative Partner mode the `(... by user)` tags tell you which were theirs). Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point. 2. **Then add the connections they would miss.** Lean in creatively — not new raw ideas, but the non-obvious links: this idea from technique one quietly solves that tension from technique four; these three are one idea wearing three hats; this wildcard is the real breakthrough. -Record the insights and chosen directions with `memlog.py append --type insight`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below. +Record the insights and chosen directions with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text ""`. **Then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`** — the session is done and must stop being offered for resume. Do this even if the user declines every artifact below. ## Artifacts diff --git a/src/core-skills/bmad-brainstorming/references/headless.md b/src/core-skills/bmad-brainstorming/references/headless.md index 1e687dfdf..5da9a25b2 100644 --- a/src/core-skills/bmad-brainstorming/references/headless.md +++ b/src/core-skills/bmad-brainstorming/references/headless.md @@ -30,8 +30,8 @@ Free-form structured payload in the first message; provide what applies: ## Run 1. Bind `{doc_workspace}` and create the memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="" [--field goal=""]`. It remains the canonical source every artifact derives from. -2. Run the divergent session per **The inversion**, capturing each idea with `memlog.py append --workspace {doc_workspace} --type idea --text ""` as it lands, and marking each technique switch with `memlog.py append --type technique --text "started "`. -3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `memlog.py append --type insight`, then run `memlog.py set --workspace {doc_workspace} --key status --value complete`. +2. Run the divergent session per **The inversion**, capturing each idea with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type idea --text ""` as it lands, and marking each technique switch with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type technique --text "started "`. +3. Synthesize: surface the conclusions, connections, and the few directions that matter; record them with `uv run {project-root}/_bmad/scripts/memlog.py append --workspace {doc_workspace} --type insight --text ""`, then run `uv run {project-root}/_bmad/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete`. 4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — the same artifacts `references/finalize.md` describes, delegating each to a subagent that reads the log as its sole source. (Headless produces the `artifacts` payload directly; it does not ask, unlike the interactive opt-in.) 5. Execute each entry in `{workflow.external_handoffs}` (capture returned URLs/IDs into the JSON `external_handoffs` array; skip and flag unavailable tools — local files always exist). Then run `{workflow.on_complete}` if non-empty. diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py deleted file mode 100644 index 5e7813829..000000000 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +++ /dev/null @@ -1,265 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = ["pytest>=8.0"] -# /// -"""Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py - -The spine under test is the flat, append-only, chronological invariant: every entry is -one line recorded at the end in the order it happened — no sections, no grouping. -""" -import json -import sys -from pathlib import Path - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -import memlog # noqa: E402 - -MEMLOG = ".memlog.md" - - -@pytest.fixture -def ws(tmp_path): - return str(tmp_path) - - -def read(ws): - return (Path(ws) / MEMLOG).read_text(encoding="utf-8") - - -def body_of(ws): - return memlog.split(read(ws))[1] - - -def entries(ws): - return [ln for ln in body_of(ws).splitlines() if ln.startswith("- ")] - - -def init(ws, **fields): - fields = fields or {"topic": "Reinvent the lunchbox", "goal": "ideas for a pitch"} - argv = ["init", "--workspace", ws] - for k, v in fields.items(): - argv += ["--field", f"{k}={v}"] - assert memlog.main(argv) == 0 - - -def append(ws, text, entry_type=None, by=None): - argv = ["append", "--workspace", ws, "--text", text] - if entry_type: - argv += ["--type", entry_type] - if by: - argv += ["--by", by] - assert memlog.main(argv) == 0 - - -# --- init --------------------------------------------------------------- - -def test_init_writes_frontmatter_fields(ws): - init(ws) - meta, body = memlog.split(read(ws)) - assert meta["topic"] == "Reinvent the lunchbox" - assert meta["goal"] == "ideas for a pitch" - assert meta["status"] == "active" - assert "updated" in meta - assert body.strip() == "" - - -def test_init_arbitrary_fields(ws): - init(ws, topic="T", audience="board") - meta, _ = memlog.split(read(ws)) - assert meta["audience"] == "board" - - -def test_init_refuses_overwrite(ws): - init(ws) - assert memlog.main(["init", "--workspace", ws, "--field", "topic=other"]) == 2 - - -def test_init_creates_missing_workspace(tmp_path): - nested = str(tmp_path / "a" / "b") - assert memlog.main(["init", "--workspace", nested, "--field", "topic=T"]) == 0 - assert (Path(nested) / MEMLOG).is_file() - - -def test_init_rejects_malformed_field(ws): - assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2 - - -# --- append: flat chronological order is the whole point ----------------- - -def test_append_lands_at_end_in_order(ws): - init(ws) - append(ws, "first") - append(ws, "second") - append(ws, "third") - assert entries(ws) == ["- first", "- second", "- third"] - - -def test_no_sections_or_headings_ever(ws): - init(ws) - append(ws, "started foo", entry_type="technique") - append(ws, "an idea", entry_type="idea") - append(ws, "started bar", entry_type="technique") - assert "## " not in body_of(ws) # the flat log never grows headings - - -def test_type_renders_as_inline_tag(ws): - init(ws) - append(ws, "the earth revolves around the sun", entry_type="idea") - append(ws, "how do we handle stampede?", entry_type="question") - body = body_of(ws) - assert "- (idea) the earth revolves around the sun" in body - assert "- (question) how do we handle stampede?" in body - - -def test_append_without_type_is_plain_note(ws): - init(ws) - append(ws, "bare entry") - assert entries(ws) == ["- bare entry"] - - -def test_append_collapses_newlines_into_one_line(ws): - init(ws) - append(ws, "line one\nline two\n spaced out") - assert entries(ws) == ["- line one line two spaced out"] - - -def test_revisited_technique_is_just_a_later_entry(ws): - # the user's model: switching techniques is an entry, not a section to return to - init(ws) - append(ws, "started SCAMPER", entry_type="technique") - append(ws, "magnetic latch", entry_type="idea") - append(ws, "started Six Hats", entry_type="technique") - append(ws, "stale data risk", entry_type="idea") - append(ws, "started SCAMPER", entry_type="technique") # back to SCAMPER — just appended again - append(ws, "stackable tiers", entry_type="idea") - assert entries(ws) == [ - "- (technique) started SCAMPER", - "- (idea) magnetic latch", - "- (technique) started Six Hats", - "- (idea) stale data risk", - "- (technique) started SCAMPER", - "- (idea) stackable tiers", - ] - - -def test_by_renders_attribution_in_tag(ws): - # Creative Partner mode must record whose idea each one was - init(ws) - append(ws, "magnetic latch lid", entry_type="idea", by="user") - append(ws, "lid doubles as a plate", entry_type="idea", by="coach") - body = body_of(ws) - assert "- (idea by user) magnetic latch lid" in body - assert "- (idea by coach) lid doubles as a plate" in body - - -def test_by_without_type_renders_alone(ws): - init(ws) - append(ws, "off-the-cuff thought", by="coach") - assert entries(ws) == ["- (by coach) off-the-cuff thought"] - - -def test_heterogeneous_entry_types_coexist(ws): - init(ws) - append(ws, "an idea", entry_type="idea") - append(ws, "an open question", entry_type="question") - append(ws, "a decision we made", entry_type="decision") - append(ws, "user wants mobile-first", entry_type="direction") - body = body_of(ws) - for tag in ("(idea)", "(question)", "(decision)", "(direction)"): - assert tag in body - - -# --- set ---------------------------------------------------------------- - -def test_set_flips_status(ws): - init(ws) - memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"]) - assert memlog.split(read(ws))[0]["status"] == "complete" - - -def test_set_preserves_body(ws): - init(ws) - append(ws, "keep me", entry_type="idea") - memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"]) - meta, body = memlog.split(read(ws)) - assert meta["status"] == "complete" - assert "- (idea) keep me" in body - - -def test_set_can_add_new_field(ws): - init(ws) - memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"]) - assert memlog.split(read(ws))[0]["owner"] == "BMad" - - -def test_updated_stays_last(ws): - init(ws) - memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"]) - meta = memlog.split(read(ws))[0] - assert list(meta)[-1] == "updated" - - -# --- robustness --------------------------------------------------------- - -def test_roundtrip_render_is_stable(ws): - init(ws) - append(ws, "one", entry_type="idea") - first = read(ws) - meta, body = memlog.split(first) - assert memlog.render(meta, body) == first - - -def test_commas_in_field_survive(ws): - init(ws, topic="cars, trains, and planes") - append(ws, "z", entry_type="idea") - meta, _ = memlog.split(read(ws)) - assert meta["topic"] == "cars, trains, and planes" - - -def test_triple_dash_in_field_does_not_corrupt_frontmatter(ws): - # A `---` inside a value must NOT be read as the closing fence: topic stays intact, - # status survives, and the body never leaks frontmatter text. - init(ws, topic="Pricing --- tiers --- and add-ons") - append(ws, "an idea", entry_type="idea") - meta, body = memlog.split(read(ws)) - assert meta["topic"] == "Pricing --- tiers --- and add-ons" - assert meta["status"] == "active" - assert entries(ws) == ["- (idea) an idea"] - assert "status:" not in body # frontmatter never bled into the body - - -def test_triple_dash_status_survives_in_ack(ws, capsys): - init(ws, topic="a --- b") - append(ws, "x", entry_type="idea") - out = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) - assert out["status"] == "active" # not "" — frontmatter recovered cleanly - - -def test_newline_in_field_is_neutralized(ws): - # A value carrying a newline can't break the fence on the next round-trip. - memlog.main(["init", "--workspace", ws, "--field", "topic=line one\nline two"]) - append(ws, "x", entry_type="idea") - meta, _ = memlog.split(read(ws)) - assert "\n" not in meta["topic"] - assert meta["status"] == "active" - - -def test_append_emits_json_ack(ws, capsys): - init(ws) - append(ws, "x", entry_type="idea") - out = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) - assert out["ok"] is True - assert out["status"] == "active" - assert out["entries"] == 1 - assert out["memlog"].endswith(MEMLOG) - assert "section" not in out # sections are gone - - -def test_ack_entry_count_climbs(ws, capsys): - init(ws) - append(ws, "a") - append(ws, "b") - out = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) - assert out["entries"] == 2