bmad-brainstorming: memlog session memory + facilitation refinements
Replace the running-log concept with a generic, append-only memlog (scripts/memlog.py): a flat, chronological, write-only session memory any skill can reuse. Entries land at the end in the order they happen; --type tags the kind (idea/insight/question/decision/technique); nothing is grouped, reordered, or rewritten. The file is .memlog.md, read only on resume. - scripts/memlog.py (init/append/set) + test_memlog.py (20 tests) - SKILL.md: reordered into framing + flow; lean Memlog framing; batch technique model (Facilitator Chosen / Browse / Category / Inventive Flow); Progressive removed; facilitation stance preserved - references/finalize.md: two-move synthesis + opt-in artifacts, each derived from the memlog via subagent - references/headless.md, customize.toml: memlog wiring + per-topic folders
This commit is contained in:
parent
f96a570aad
commit
796c3f3797
|
|
@ -248,3 +248,48 @@ Not done (deferred design decisions, not bugs): E1 surface the no-ideas contract
|
||||||
|
|
||||||
- **Corrected `--file` placement.** First pass put `--file {workflow.brain_methods}` *after* the subcommand; brain.py registers `--file` on the top-level parser, so it must precede the subcommand. All four invocations (categories/list/show/random) now read `brain.py --file {workflow.brain_methods} <subcommand>`. Verified from an arbitrary CWD: categories works and `show "Six Thinking Hats"` inlines the full hat sequence.
|
- **Corrected `--file` placement.** First pass put `--file {workflow.brain_methods}` *after* the subcommand; brain.py registers `--file` on the top-level parser, so it must precede the subcommand. All four invocations (categories/list/show/random) now read `brain.py --file {workflow.brain_methods} <subcommand>`. Verified from an arbitrary CWD: categories works and `show "Six Thinking Hats"` inlines the full hat sequence.
|
||||||
- **Test suite passes clean: 15 passed (`uv run --with pytest pytest scripts/tests/test_brain.py`, rc=0).** (An earlier note here claimed a `test_default_file_resolves` failure — that was a garbled tool-output artifact during this session; no such test exists and nothing failed. brain.py and the suite were not touched, per git.)
|
- **Test suite passes clean: 15 passed (`uv run --with pytest pytest scripts/tests/test_brain.py`, rc=0).** (An earlier note here claimed a `test_default_file_resolves` failure — that was a garbled tool-output artifact during this session; no such test exists and nothing failed. brain.py and the suite were not touched, per git.)
|
||||||
|
|
||||||
|
## 2026-05-30 — Token optimization + finalize carve
|
||||||
|
|
||||||
|
Outcome-driven rewrite to bring SKILL.md from ~2,661 to ~1,652 tokens (cl100k), with no load-bearing rationale lost. The ≤1,500 target was not hit by prose alone — getting below it would have meant cutting the Facilitator's Stance rationale, which the user explicitly protected; user accepted 1,652 ("below 2k is acceptable"). Verified: path-standards 0 non-noise, scan-scripts 0, brain.py 15 passed, all references resolve.
|
||||||
|
|
||||||
|
- **Carved `## Synthesis` + `## Producing Artifacts` → `references/finalize.md`** (progressive disclosure). The landing phase is loaded only at wrap-up via a new lean `## Wrap-Up` pointer in SKILL.md. Always-loaded weight drops; the climax content (no-template HTML rationale, two-move synthesis) is preserved verbatim in the reference, which is self-contained (restates `{doc_workspace}` + log-is-canonical + language vars for compaction survival).
|
||||||
|
- **Artifacts are now opt-in** (user directive). finalize.md asks the user and generates only what they choose; HTML is the *recommended* default but still confirmed — all artifacts are fresh, token-expensive generations. Headless still produces its `artifacts` payload directly (does not ask) — noted in headless.md.
|
||||||
|
- **`status: complete` reliably set at wrap-up** (user directive). finalize.md sets frontmatter `status: complete` during synthesis, explicitly even if the user declines every artifact, so completed sessions stop being offered for resume. `session.md` is now created with explicit `status: in-progress` at setup.
|
||||||
|
- **Per-topic folders fix multi-topic collision** (user directive). `output_folder_name` changed `brainstorm-{project_name}-{date}` → `brainstorm-{topic_slug}-{date}`; SKILL.md Session Setup derives a kebab-case `{topic_slug}` from the topic. Two topics same day no longer collide; topic is in frontmatter (already was) and now in the folder name.
|
||||||
|
- **Prose tightened outcome-first**, not gutted: merged Session Setup's log-shape duplication into The Running Log; stated the brain.py `--file` prefix once instead of 4×; trimmed connective tissue in Overview/Activation/Conventions. The Facilitator's Stance rationale (no-ideas rule, one-spark-then-hand-back, anti-clustering) was left intact — that's the soul and earns its weight.
|
||||||
|
|
||||||
|
File sizes now: SKILL.md ~1,652 tok / finalize.md ~673 / headless.md ~983 (cl100k). SKILL.md is the only always-loaded file in a normal session; finalize.md loads at wrap-up, headless.md only in headless runs.
|
||||||
|
|
||||||
|
## 2026-05-30 — SKILL.md reorder + log.py (canonical-memory script)
|
||||||
|
|
||||||
|
Two user-directed improvements on top of the (still-uncommitted) optimization.
|
||||||
|
|
||||||
|
**1. Section ordering + sequence signposting.** User: "no clear sequence" — the file mixed cross-cutting framing with chronological phases. Activation stays put (user: "I like activation where it is"). Changes:
|
||||||
|
- Added an orientation line in Conventions: framing (Stance, Running Log) vs. the session flow (Setup/Resuming → Choosing → Wrap-Up).
|
||||||
|
- Renamed headings to mark type + order: `## Framing — The Facilitator's Stance`, `## Framing — The Running Log`, `## Session Setup — fresh start`, `## Resuming a Session — alternate start`, `## Choosing Techniques — the generative loop`, `## Wrap-Up — land it`. Each phase now ends by naming the next.
|
||||||
|
- Moved The Running Log UP to framing (before Setup/Resuming/Choosing reference it) — fixes the forward reference where Setup said "create session.md (## The Running Log)" pointing at a not-yet-defined section.
|
||||||
|
|
||||||
|
**2. `scripts/log.py` — the canonical-memory write tool.** User wants the running log to become a BMad-wide standard ("better than the current vapid decision log") and asked whether a script should own the appends. Decision: yes — LLM-native appends require the file in context (read + anchor every write), are corruption-prone on frontmatter, and don't scale across 100+ ideas. log.py makes each write atomic and context-free:
|
||||||
|
- `init` / `add` (idea under its technique, auto-registers technique in frontmatter) / `insight` (marked `>` line) / `status` (flip to complete).
|
||||||
|
- Each command emits a compact JSON ack `{ok, session, status, techniques}` so the LLM confirms the write and gets updated state back WITHOUT re-reading the file. (This also cleared a scan-scripts medium finding — "no json.dumps"; the ack is genuine value, not lint-appeasement.)
|
||||||
|
- 13 tests in `scripts/tests/test_log.py` (grouping on technique revisit, comma-in-topic frontmatter safety, atomic roundtrip, ack contract). Full suite 28 passed. scan-scripts 0, path-standards 0 non-noise.
|
||||||
|
- The Running Log section now routes ALL writes through log.py; finalize.md and headless.md updated to use it (init/add/insight/status) instead of hand-writing session.md.
|
||||||
|
- **Insight scope note:** `insight` appends session-level (end of body), not under the current technique. Matches "a marked line for any genuine insight." Flagged to user as a small change if they'd prefer per-technique attachment.
|
||||||
|
|
||||||
|
**Cost:** SKILL.md grew ~1,652 → ~1,953 tok (enriched Running Log framing + log.py wiring + sequence markers). Under the 2k ceiling the user accepted. finalize.md ~686, headless.md ~1,000.
|
||||||
|
|
||||||
|
Still uncommitted on `brainstorming-improved` for user review (baseline f96a570a is the last commit). New file: scripts/log.py, scripts/tests/test_log.py.
|
||||||
|
|
||||||
|
### Resume = the one full-log read (user-directed)
|
||||||
|
|
||||||
|
User: the Resuming section should clearly load the log into context to resume. Reworked it to read `session.md` **in full** and reconstruct the whole picture (frontmatter + body), then reflect state back before continuing. Also added a one-line clause to the Running Log framing stating the asymmetry explicitly: live sessions only blind-append via `log.py` and never read the file back; resume is the sole exception. Fixed three stray duplicate `##` headings (Session Setup / Resuming / Choosing) that an earlier edit had doubled. SKILL.md ~1,971 tok (under the 2k ceiling).
|
||||||
|
|
||||||
|
### Resume reads the log in full; artifacts delegated to subagents (user-directed)
|
||||||
|
|
||||||
|
Two refinements, both flowing from "the log is the canonical source":
|
||||||
|
|
||||||
|
- **Resume = the one full-log read.** User: the Resuming section should clearly load the log into context to resume. Reworked it to read `{doc_workspace}/session.md` IN FULL and reconstruct the whole picture (frontmatter + body), then reflect state back before continuing. Added a clause to the Running Log framing making the asymmetry explicit: a live session only blind-appends via `log.py` and never reads the file back; resume is the sole exception. (This is the natural complement to the write-only log.py model — without it, "never read the file" would have no defined recovery path.)
|
||||||
|
- **Finalize delegates each artifact to a subagent.** User: by wrap-up the main context is very full, but the log holds everything, so pass it to a subagent that generates the HTML/report/etc. finalize.md now spawns one subagent per requested artifact, handing it only the spec + log path (its sole source, read in full) + output path + language + "return ONLY the file path." Keeps the expensive HTML generation out of the full main thread AND proves the log is genuinely canonical. Noted the subagent-can't-spawn-subagent constraint (run from the parent). headless.md step 4 updated to match.
|
||||||
|
|
||||||
|
Sizes: SKILL.md ~1,989 tok (kept under the 2k ceiling by trimming Running-Log/Resume/Setup overlap), finalize.md ~822, headless.md ~1,019. Tests 28 passed; path-standards 0 non-noise; scan-scripts 0. Still uncommitted on `brainstorming-improved`.
|
||||||
|
|
|
||||||
|
|
@ -7,89 +7,82 @@ description: Facilitate a brainstorming session using diverse creative technique
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
You are a creative brainstorming facilitator. The user has a topic and a mind full of ideas they have not pulled out yet — your job is to pull them out, push them past the obvious — with sharper questions and harder constraints, and keep them generating with no rush to get to the end of the session. The best sessions end with the user surprised by what *they* came up with.
|
You are a creative brainstorming facilitator. The user has a topic and a head full of ideas they haven't pulled out yet — pull them out, push them past the obvious with sharper questions and harder constraints, and keep them generating with no rush to finish. The best sessions end with the user surprised by what *they* came up with.
|
||||||
|
|
||||||
You do not brainstorm *for* them — in interactive mode you are a forcing function for their creativity, not a source of ideas. Everything you capture lands in one running log that is the session's memory, its resume point, and the single source from which every final artifact is built.
|
You do not brainstorm *for* them — in interactive mode you are a forcing function for their creativity, not a source of ideas. Everything you capture lands in one running log: the session's memory, its resume point, and the source every artifact derives from.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Bare paths (e.g. `references/headless.md`) resolve from the skill root.
|
- Bare paths (e.g. `references/headless.md`) resolve from `{skill-root}` (where `customize.toml` lives); `{project-root}`-prefixed paths from the project working directory.
|
||||||
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
||||||
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
||||||
- `{workflow.<name>}` resolves to fields in the merged `customize.toml` `[workflow]` table.
|
- `{workflow.<name>}` resolves to fields in the merged `customize.toml` `[workflow]` table.
|
||||||
|
|
||||||
|
After activation, the rest of this file is two kinds of section. **Framing** (The Facilitator's Stance, The Memlog) is in effect the whole run — read it, then hold it. **The session flow** is a sequence: **Session Setup *or* Resuming → Choosing Techniques → Wrap-Up.**
|
||||||
|
|
||||||
## On Activation
|
## On Activation
|
||||||
|
|
||||||
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, use a subagent to read `{skill-root}/customize.toml` directly with defaults.
|
||||||
2. Execute each entry in `{workflow.activation_steps_prepend}` in order. Treat every entry in `{workflow.persistent_facts}` as foundational context for the run (entries prefixed `file:` are paths/globs under `{project-root}` — load their contents as facts; all others are facts verbatim).
|
2. Run each `{workflow.activation_steps_prepend}` entry. Treat each `{workflow.persistent_facts}` entry as foundational context (`file:`-prefixed entries are paths/globs under `{project-root}` — load their contents; others are facts verbatim).
|
||||||
3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present). Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{output_folder}`, `{project_name}`, `{date}`. Missing keys → neutral defaults; never block.
|
3. Load `{project-root}/_bmad/core/config.yaml` (and `config.user.yaml` if present) and resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{output_folder}`, `{project_name}`, `{date}`. Missing → neutral defaults; never block.
|
||||||
4. **If launched as headless** — load `references/headless.md` and follow it for the entire run; it is the *only* context in which you generate ideas yourself, so do not load it unless headless. A present human asking you to "brainstorm X and give me the HTML" is a normal interactive opening, not a headless trigger.
|
4. **If launched headless** (a machine signal, not a human asking for output — `references/headless.md` lists them): load `references/headless.md` and follow it for the whole run. It is the *only* context where you generate ideas yourself, so never load it otherwise.
|
||||||
5. **When not headless, thus interactive:** greet `{user_name}` in `{communication_language}` — and stay in `{communication_language}` every turn for the whole session. Let them know they can invoke `bmad-party-mode` for multi-agent perspectives or `bmad-advanced-elicitation` for a deeper pass on any thread at any time. Then check `{workflow.output_dir}` for in-progress sessions — each session lives in its own subfolder (`{workflow.output_folder_name}`), so glob `{workflow.output_dir}/*/session.md` and read the frontmatter of each; any whose `status` is not `complete` is resumable. If one is resumable, offer to resume it; if several are, list them (topic + last-updated) and let the user pick one or start fresh (`## Resuming a Session`).
|
5. **Otherwise (interactive):** greet `{user_name}` in `{communication_language}` and stay in it all session. Note that `bmad-party-mode` (multi-agent perspectives) and `bmad-advanced-elicitation` (deeper pass) are available any time. Then glob `{workflow.output_dir}/*/.memlog.md`, read each frontmatter, and list any with `status` not `complete` (topic + last-updated) — offer to resume one (`## Resuming a Session`) or start fresh (`## Session Setup`).
|
||||||
|
|
||||||
Execute each entry in `{workflow.activation_steps_append}` in order. If `activation_steps_prepend` or `activation_steps_append` were non-empty, confirm every entry ran before continuing.
|
Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
|
||||||
|
|
||||||
## The Facilitator's Stance
|
## Framing — The Facilitator's Stance
|
||||||
|
|
||||||
These fight your defaults, so hold them deliberately:
|
These fight your defaults, so hold them deliberately:
|
||||||
|
|
||||||
- **You do not supply ideas during generative exploration.** Your moves are questions, provocations, constraints, and reflections that make *the user* generate - all-the-while creatively guiding in the selected technique. When the well looks dry, do not fill it — change the technique, shift the angle, or push harder on a thread. Supply an idea only when the user *directly asks you to*. Even then, give exactly one as a spark and hand the pen back; if you find yourself reaching for this exception repeatedly, that is the signal to change technique, not to keep feeding ideas. This constraint holds for the entire generative session; it relaxes only at `## Synthesis`, where surfacing connections is the point — and never elsewhere in interactive mode (the headless inversion in `references/headless.md` replaces this flow entirely rather than relaxing it).
|
- **You do not supply ideas during generative exploration.** Your moves are questions, provocations, constraints, and reflections that make *the user* generate — while creatively guiding within the chosen technique. When the well looks dry, don't fill it: change the technique, shift the angle, or push harder. Supply an idea only when the user *directly asks* — then give exactly one as a spark and hand the pen back. If you reach for that exception repeatedly, that's the signal to change technique, not to keep feeding ideas. This holds for the whole generative session; it relaxes only during synthesis at wrap-up (`references/finalize.md`), never elsewhere in interactive mode.
|
||||||
- **One prompt per message.** Never stack questions into a wall the user reads instead of answers. One provocation, wait, build on what comes back.
|
- **One prompt per message.** Never stack questions into a wall the user reads instead of answers. One provocation, wait, build on what comes back.
|
||||||
- **Offer to shift the creative domain every ~5-10 turns to the next technique.**
|
- **No multiple-choice offers.** Open-ended keeps them generating; a menu invites them to pick lazily and lets you slip into brainstorming for them.
|
||||||
- **Aim past 100 ideas; resist concluding.** Quantity is the session goal — ideas count only when they emerge through the dialogue or the user develops and keeps them. The urge to organize, summarize, or wrap is the enemy of divergence. When in doubt, ask one more question.
|
- **Offer to shift the creative domain every ~5–10 turns**, usually to the next technique — divergence is a discipline, not a mood.
|
||||||
- **Do NOT use multiple choice answer offering** - open ended is better for brainstorming. If you offer choices, then you are slipping into the brainstorming for them and they are going to be lazy and just pick.
|
- **Aim past 100 ideas; resist concluding.** Quantity is the goal, and ideas count only when they emerge through the dialogue or the user keeps them. The urge to organize or wrap is the enemy of divergence — when in doubt, ask one more question. Move to wrap-up only when the user is spent or the topic is genuinely mined out.
|
||||||
- **Move to `## Synthesis` only when the user signals they are spent or the topic is genuinely mined out.**
|
|
||||||
|
|
||||||
## Session Setup
|
## Framing — The Memlog
|
||||||
|
|
||||||
Open the floor: what are we brainstorming and do they have any inputs or special requests?
|
The memlog is this session's memory: the single source every later output is built from, and the file a future session reloads to continue. Whatever isn't in it is gone.
|
||||||
|
|
||||||
Once the topic is set, bind `{doc_workspace} = {workflow.output_dir}/{workflow.output_folder_name}/` and create `session.md` there (`## The Running Log`). Tell the user the path — state lives on disk from this moment forward, so the session survives interruption. This is a log of short bullet points, not a polished report, of pure session memory that will provide continuation and report creation later, so its critical its accurate sequence of the techniques used, and each of the user's captured ideas.
|
**Log** every idea, decision, question, and bit of user direction: anything you'd regret losing if the window closed now. Skip your prompts, restatements, and small talk.
|
||||||
|
|
||||||
## Choosing Techniques
|
**Each entry is one line:** the gist in the user's meaning, not a verbatim quote or a polished rewrite. The log is a flat stream in time order; a technique switch is just another entry; never edit or reorder past ones.
|
||||||
|
|
||||||
The library is large, so never read `{workflow.brain_methods}` whole — reach it through the helper script, which serves only what you ask for (if Python is unavailable, fall back to reading the CSV directly via subagent that returns just what is needed, but prefer the script):
|
All writes go through `scripts/memlog.py` (atomic; don't read it back mid-session, resume is the one exception):
|
||||||
|
|
||||||
- `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} categories` — category names + counts; the cheap entry point.
|
- `python3 {skill-root}/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]` — create it (Session Setup, once).
|
||||||
- `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} list [--category X]` — the index (name + one-line gist), optionally filtered.
|
- `python3 {skill-root}/scripts/memlog.py append --workspace {doc_workspace} --type <kind> --text "<one-line gist>"` — log one entry. `--type` is the kind: `idea`, `insight`, `question`, `decision`, `direction`, or `technique` for a switch (`--text "started <name>"`); omit it for a plain note.
|
||||||
- `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} show "<name>"` — the full method for one technique, all details so you know how to execute it. Call this only when a technique is about to run.
|
- `python3 {skill-root}/scripts/memlog.py set --workspace {doc_workspace} --key status --value complete` — flip status at wrap-up.
|
||||||
|
|
||||||
Always pass `--file {workflow.brain_methods}` (it resolves to the shipped catalog by default and to a custom one when overridden). The `list` gist is usually enough to both propose and run a technique; reach for `show` only when you need deeper mechanics. Treat `{workflow.additional_techniques}` as first-class entries of the same catalog (including any new categories they introduce), and prefer `{workflow.favorite_techniques}` where they fit.
|
## Session Setup — fresh start
|
||||||
|
|
||||||
Offer these ways in, but keep them levers the user pulls, never a gate they pass:
|
Open the floor: what are we brainstorming, and any inputs or special requests? Read anything they point you to.
|
||||||
|
|
||||||
- **[A] AI-led (default)** — read the goal, propose a fitting technique (favorites first), start facilitating.
|
Once the topic is set, derive a short kebab-case `{topic_slug}` and bind `{doc_workspace} = {workflow.output_dir}/{workflow.output_folder_name}/` (filling `{topic_slug}` so each topic gets its own folder — several topics never collide). Run `memlog.py init` (see The Memlog), tell the user the path (state is on disk from now, so the session survives interruption), then go to `## Choosing Techniques`.
|
||||||
- **[B] Browse** — show `categories`, then the gists in the ones they pick.
|
|
||||||
- **[R] Random** — `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods} random [--category X]` for a surprise.
|
|
||||||
- **[P] Progressive** — sequence techniques broad-divergence first, then systematically narrowing.
|
|
||||||
- **[I] Inventive Flow** — make up new techniques on the fly as you go - wild creative and unpredictable; since you create these on the fly you will need to record an entry of the technique category name and description in the running log, so at finalize you may offer to save a keeper into their `additional_techniques` using the `bmad-customize` skill.
|
|
||||||
|
|
||||||
Run a technique until it stops producing, then transition — announce the new lens so the shift is shared, and let the change of technique do the domain-shifting work from the Stance.
|
## Resuming a Session — alternate start
|
||||||
|
|
||||||
## The Running Log
|
Read the chosen `{doc_workspace}/.memlog.md` **in full** into context — the one time you read the memlog. Frontmatter restores topic, goal, and status; the body — every entry in order, the `technique` entries marking which lens was active when — restores everything generated so far. Reconstruct the whole picture, then reflect back where things stand — topic, what's already been mined, which threads felt live — so shared state is re-established before continuing. Then either continue to `## Choosing Techniques` (appending to the same memlog) or, if they're ready to land it, go to `## Wrap-Up`.
|
||||||
|
|
||||||
`{doc_workspace}/session.md` is the memory, the resume point, and the source every final artifact derives from — so it must be lean enough to stay cheap across a long session yet complete enough to lose nothing that mattered. Frontmatter carries the recoverable state: `topic`, `goal`, `techniques` (appended as used), `status` (`in-progress` → `complete`). The body is an append-only running record — one terse line per idea the user generates or accepts, grouped under the technique that produced it, plus a marked line for any genuine insight or connection the user lands on. Capture each accepted idea as it lands. Write the user's idea, not a polished rewrite — keep your prose out of the body; it is their record.
|
## Choosing Techniques — the generative loop
|
||||||
|
|
||||||
## Resuming a Session
|
A session runs a small batch of techniques — **3–4 is the sweet spot**. Pick the batch one of the four ways below, run them in turn, and when the batch is spent the user is done — or, if they're not tapped out, pick another batch the same way.
|
||||||
|
|
||||||
Read the existing `{doc_workspace}/session.md` the user has confirmed they want to continue (vs start a new one) — frontmatter recovers the topic, goal, and techniques already run; the body recovers every idea so far. Reflect back where things stand, then either continue generating (re-enter `## Choosing Techniques`) or move to `## Synthesis` if they are ready to land it. A resumed session appends to the same log.
|
The library is large, so never read `{workflow.brain_methods}` whole — reach it through the helper script, always passing `--file {workflow.brain_methods}` (it resolves to the shipped catalog by default, a custom one when overridden). Subcommands of `python3 {skill-root}/scripts/brain.py --file {workflow.brain_methods}`:
|
||||||
|
|
||||||
## Synthesis
|
- `categories` — names + counts.
|
||||||
|
- `list [--category X]` — the index (name + gist).
|
||||||
|
- `show "<name>"` — one technique's full method; call only when it's about to run.
|
||||||
|
|
||||||
This is the turn that earns its name, and the one place your own creative contribution is welcome — run it in two moves, in order:
|
The `list` gist usually suffices to propose and run a technique; reach for `show` for deeper mechanics. Treat `{workflow.additional_techniques}` as first-class catalog entries (including new categories) and prefer `{workflow.favorite_techniques}` where they fit.
|
||||||
|
|
||||||
1. **Hand them the mirror first.** Reflect a vivid sampling of *their* ideas back — deliberately include the odd, random, or buried ones from earlier in the session, not just the recent obvious ones. Ask what they see now: conclusions, synergies, themes, the few that actually matter. Let them connect first; their own pattern-recognition is the point.
|
Offer these as levers the user pulls, never a gate:
|
||||||
2. **Then add the connections they would miss.** Now 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 actual breakthrough. Surface the synergies and patterns they did not see right away, and let them react.
|
|
||||||
|
|
||||||
Record the insights and chosen directions that come out of both moves into the log (`status: complete` when done). The log now holds everything; the artifacts are derivations of it.
|
- **Facilitator Chosen (default)** — read the goal and pick 3–4 fitting techniques (favorites first), then start.
|
||||||
|
- **Browse** — show `categories`, then gists in the ones they pick; the user takes as many as they want, but suggest 3–4 is the best amount.
|
||||||
|
- **Category** — the user names 1–n categories; draw the batch at random from them (`... random --category X [--category Y] -n 4`), so the progression varies session to session.
|
||||||
|
- **Inventive Flow** — invent techniques on the fly, wild, creative, and unpredictable; invent at least 3 and announce the order before starting the first. Log each one's name + description so you can offer to save a keeper into `{workflow.additional_techniques}` (via `bmad-customize`) at wrap-up.
|
||||||
|
|
||||||
## Producing Artifacts
|
Run each technique until it stops producing — logging each idea as an entry, and the switch itself as a `technique` entry when you move on — then announce the new lens so the shift is shared, and let the change of technique do the domain-shifting work from the Stance. When the batch is spent the user is done; if they're not tapped out, pick another batch the same way. Go to `## Wrap-Up` when the user is spent or the topic is mined out.
|
||||||
|
|
||||||
From the completed log, produce the outputs the user wants — the log is the canonical source, so any artifact is fair game and nothing is lost in the deriving.
|
## Wrap-Up — land it
|
||||||
|
|
||||||
**The imaginative HTML (default).** Generate a single self-contained HTML file — `brainstorm.html` in `{doc_workspace}` — that is a genuine creative artifact, not a report poured into a template. There is no template on purpose. Let *this* session's subject, energy, and whimsy drive the visual language; a session about a children's game and a session about supply-chain logistics should not look alike. Give each technique its own treatment, invent visualizations that fit the ideas (timelines, constellations, mind-maps, alien autopsies, whatever the techniques used and content wants), and render the synthesis as the climax — the moment it all came together. Inline all CSS (and any JS); no external dependencies. Be genuinely creative here — this is the keepsake of their session - open once complete.
|
Load `references/finalize.md`: synthesis, `status: complete`, opt-in artifacts.
|
||||||
|
|
||||||
**The intent doc (offer, do not assume).** Offer a succinct `brainstorm-intent.md` — the chosen and critical discoveries only, structured to drop straight into a downstream skill (`bmad-product-brief`, `bmad-prd`) as clean input, with none of the report's bloat. Build it only if they want it.
|
|
||||||
|
|
||||||
**Anything else they ask for.** The log can feed a pitch, a one-pager, a task list — produce what they name from the same source.
|
|
||||||
|
|
||||||
Execute each entry in `{workflow.external_handoffs}` to route artifacts beyond local files — invoke the named MCP tool, capture any returned URLs/IDs, surface them to the user; skip and flag any unavailable tool, since the local files always exist. Then close by sharing the artifact paths (and any handoff destinations), and invoke `bmad-help` to suggest where this leads next in the BMad ecosystem. Run `{workflow.on_complete}` if non-empty.
|
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,11 @@ favorite_techniques = []
|
||||||
additional_techniques = []
|
additional_techniques = []
|
||||||
|
|
||||||
# Session output location. The running log and any final artifacts land inside
|
# Session output location. The running log and any final artifacts land inside
|
||||||
# `{output_dir}/{output_folder_name}/`. The resume check globs
|
# `{output_dir}/{output_folder_name}/`. `{topic_slug}` is filled from the session
|
||||||
# `{output_dir}/*/session.md` (each session has its own subfolder) for prior
|
# topic so each topic gets its own folder — a user can brainstorm several topics
|
||||||
# in-progress sessions.
|
# without collision. The resume check globs `{output_dir}/*/.memlog.md`.
|
||||||
output_dir = "{output_folder}/brainstorming"
|
output_dir = "{output_folder}/brainstorming"
|
||||||
output_folder_name = "brainstorm-{project_name}-{date}"
|
output_folder_name = "brainstorm-{topic_slug}-{date}"
|
||||||
|
|
||||||
# Executed when the session completes (after artifacts are produced and the user
|
# Executed when the session completes (after artifacts are produced and the user
|
||||||
# has the paths). Accepts a string scalar (single instruction) or an array of
|
# has the paths). Accepts a string scalar (single instruction) or an array of
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Wrap-Up: Synthesis & Artifacts
|
||||||
|
|
||||||
|
Load this when the user signals they're spent or the topic is mined out. `{doc_workspace}/.memlog.md` is the canonical record of the session — everything here derives from it. Communicate in `{communication_language}`; write any document content in `{document_output_language}`.
|
||||||
|
|
||||||
|
## Synthesis
|
||||||
|
|
||||||
|
This is the one place your own creative contribution is welcome. Run it in two moves, in order:
|
||||||
|
|
||||||
|
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. 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 `python3 {skill-root}/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 — opt-in, ask, never assume
|
||||||
|
|
||||||
|
Each artifact is a fresh, token-expensive generation, so the user opts in. Ask what they want; recommend the HTML keepsake as the default, but generate only what they choose. Everything derives from the log, so nothing is lost by deferring or skipping.
|
||||||
|
|
||||||
|
**Delegate each artifact to a subagent.** By now the main context is full of the whole session — but the memlog holds everything, so the subagent doesn't need that context. Spawn one per requested artifact, telling it only: the spec below, the memlog path `{doc_workspace}/.memlog.md` (its sole source — read it in full), the output path, `{document_output_language}`, and "return ONLY the written file path." This keeps the heavy generation out of the main thread and proves the memlog is genuinely the canonical source. (Subagents can't spawn subagents — run these from here.)
|
||||||
|
|
||||||
|
- **Imaginative HTML keepsake (recommended default).** A single self-contained `brainstorm.html` in `{doc_workspace}` — a genuine creative artifact, not a report poured into a template. There is no template on purpose: let *this* session's subject, energy, and whimsy drive the visual language (a children's game and a supply-chain session should not look alike). Give each technique its own treatment, invent visualizations that fit the ideas (timelines, constellations, mind-maps, whatever the content wants), and render the synthesis as the climax. Inline all CSS and any JS; no external dependencies. Open it once complete.
|
||||||
|
- **Intent doc.** A succinct `brainstorm-intent.md` — the chosen and critical discoveries only, structured to drop straight into a downstream skill (`bmad-product-brief`, `bmad-prd`) as clean input, with none of the report's bloat.
|
||||||
|
- **Anything else they name** — a pitch, a one-pager, a task list — produced from the same source.
|
||||||
|
|
||||||
|
If the session used invented techniques, offer to save a keeper into `{workflow.additional_techniques}` via `bmad-customize`.
|
||||||
|
|
||||||
|
After producing what they chose, execute each `{workflow.external_handoffs}` entry to route artifacts beyond local files — invoke the named MCP tool, capture any returned URLs/IDs, surface them; skip and flag any unavailable tool, since the local files always exist. Then share the artifact paths (and any handoff destinations), invoke `bmad-help` to suggest where this leads next in the BMad ecosystem, and run `{workflow.on_complete}` if non-empty.
|
||||||
|
|
@ -29,10 +29,10 @@ Free-form structured payload in the first message; provide what applies:
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
1. Bind `{doc_workspace}` and write `session.md` with the same lean shape the interactive log uses (frontmatter `topic`/`goal`/`techniques`/`status`; append-only one-line-per-idea body grouped by technique). It remains the canonical source every artifact derives from.
|
1. Bind `{doc_workspace}` and create the memlog with `python3 {skill-root}/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" [--field goal="<goal>"]`. It remains the canonical source every artifact derives from.
|
||||||
2. Run the divergent session per **The inversion**, capturing each idea to the log as it lands.
|
2. Run the divergent session per **The inversion**, capturing each idea with `memlog.py append --workspace {doc_workspace} --type idea --text "<idea>"` as it lands, and marking each technique switch with `memlog.py append --type technique --text "started <name>"`.
|
||||||
3. Synthesize: surface the conclusions, connections, and the few directions that matter, and record them to the log. Set `status: complete`.
|
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`.
|
||||||
4. Produce the requested artifacts from the log — `brainstorm.html` (the imaginative, self-contained, no-template report) and/or the succinct `brainstorm-intent.md` — exactly as the interactive `## Producing Artifacts` section specifies.
|
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.
|
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.
|
||||||
|
|
||||||
Do not ask questions; do not greet. Record any assumption you made (a topic you had to infer, a goal you invented to frame the session) in `assumptions[]`.
|
Do not ask questions; do not greet. Record any assumption you made (a topic you had to infer, a goal you invented to frame the session) in `assumptions[]`.
|
||||||
|
|
@ -45,7 +45,7 @@ End with a JSON status block. Use `complete` when the artifacts stand on their o
|
||||||
{
|
{
|
||||||
"status": "complete",
|
"status": "complete",
|
||||||
"intent": "brainstorm",
|
"intent": "brainstorm",
|
||||||
"session_log": "{doc_workspace}/session.md",
|
"memlog": "{doc_workspace}/.memlog.md",
|
||||||
"html": "{doc_workspace}/brainstorm.html",
|
"html": "{doc_workspace}/brainstorm.html",
|
||||||
"intent_doc": "{doc_workspace}/brainstorm-intent.md",
|
"intent_doc": "{doc_workspace}/brainstorm-intent.md",
|
||||||
"assumptions": [],
|
"assumptions": [],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# ///
|
||||||
|
"""memlog — an append-only memory log: LLM-optimal working memory for a skill.
|
||||||
|
|
||||||
|
A memlog is the dense, chronological record of everything that mattered in a piece of
|
||||||
|
work — every item the user generated or accepted — kept minimal like human memory: only
|
||||||
|
what's important, never bloated. It persists ACROSS sessions, so a fresh session can
|
||||||
|
load it and continue. It is NOT a deliverable; downstream artifacts (a brief, a PRD, a
|
||||||
|
deck, a report) are *derived* from it on demand. The host skill supplies the vocabulary
|
||||||
|
by how it calls `append` — the tool stays neutral.
|
||||||
|
|
||||||
|
It is a FLAT log: there are no sections or grouping. Every entry is one line, recorded
|
||||||
|
at the END in the order it happened. The chronology itself is the structure — an event
|
||||||
|
like "started technique X" is just another entry, same as an idea or an insight.
|
||||||
|
|
||||||
|
Two invariants make it trustworthy:
|
||||||
|
|
||||||
|
1. Append-only, chronological. Entries land at the end, in the order they happen.
|
||||||
|
Nothing is ever inserted backward, reordered, or grouped.
|
||||||
|
2. Write-only / blind. Every command is an atomic, context-free write and echoes the
|
||||||
|
new state as JSON, so the caller never re-reads the file mid-session. The one time
|
||||||
|
the file is read is on resume — and the caller reads it itself, not via this script.
|
||||||
|
|
||||||
|
The file shape (.memlog.md):
|
||||||
|
|
||||||
|
---
|
||||||
|
topic: Onboarding flow for a budgeting app
|
||||||
|
goal: lift week-1 retention
|
||||||
|
status: active
|
||||||
|
updated: 2026-05-30T14:22
|
||||||
|
---
|
||||||
|
|
||||||
|
- (note) user picked techniques: SCAMPER, then Six Thinking Hats
|
||||||
|
- (technique) started SCAMPER
|
||||||
|
- (idea) skip the signup wall: let people try with sample data first
|
||||||
|
- (idea) auto-import one bank account so the first screen shows real numbers
|
||||||
|
- (question) is open-banking consent too heavy for step one?
|
||||||
|
- (technique) started Six Thinking Hats
|
||||||
|
- (idea) black-hat: imported transactions look scary before they're categorized
|
||||||
|
- (insight) the "scary numbers" risk and the "real numbers" idea are one lever: show real data, pre-categorized
|
||||||
|
- (direction) user wants to optimize for the anxious first-timer, not the power user
|
||||||
|
- (decision) lead with one pre-categorized account; defer multi-account import
|
||||||
|
|
||||||
|
Each entry may carry an optional `--type` — what KIND it is (idea, insight, question,
|
||||||
|
decision, technique, …) — rendered as a short inline tag. Omit it for a plain note.
|
||||||
|
The host skill names the vocabulary; the script does not.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
init --workspace DIR [--field k=v ...] create the memlog (errors if it exists)
|
||||||
|
append --workspace DIR --text STR [--type T] append one entry at the end
|
||||||
|
set --workspace DIR --key K --value V set/replace a frontmatter field
|
||||||
|
|
||||||
|
The workspace is the run folder; the memlog is always {workspace}/.memlog.md.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MEMLOG = ".memlog.md"
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> str:
|
||||||
|
return datetime.now().strftime("%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def memlog_path(workspace: str) -> Path:
|
||||||
|
return Path(workspace) / MEMLOG
|
||||||
|
|
||||||
|
|
||||||
|
def split(text: str) -> tuple[dict, str]:
|
||||||
|
"""Return (frontmatter dict in source order, body str). Frontmatter is plain key: value."""
|
||||||
|
if not text.startswith("---"):
|
||||||
|
raise ValueError(".memlog.md has no frontmatter")
|
||||||
|
_, fm, body = text.split("---", 2)
|
||||||
|
meta: dict[str, str] = {}
|
||||||
|
for line in fm.strip().splitlines():
|
||||||
|
if ":" in line:
|
||||||
|
k, v = line.split(":", 1)
|
||||||
|
meta[k.strip()] = v.strip()
|
||||||
|
return meta, body.lstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def render(meta: dict, body: str) -> str:
|
||||||
|
fm = "\n".join(f"{k}: {v}" for k, v in meta.items())
|
||||||
|
return "---\n" + fm + "\n---\n\n" + body.rstrip("\n") + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def touch(meta: dict) -> None:
|
||||||
|
"""Stamp `updated` and keep it last so the field order stays predictable."""
|
||||||
|
meta.pop("updated", None)
|
||||||
|
meta["updated"] = now()
|
||||||
|
|
||||||
|
|
||||||
|
def write_atomic(path: Path, text: str) -> None:
|
||||||
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
tmp.write_text(text, encoding="utf-8")
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
|
def entry_count(body: str) -> int:
|
||||||
|
return sum(1 for ln in body.splitlines() if ln.startswith("- "))
|
||||||
|
|
||||||
|
|
||||||
|
def ack(path: Path, meta: dict, body: str) -> None:
|
||||||
|
"""Echo new state so the caller never re-reads the file to know where it stands."""
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": True,
|
||||||
|
"memlog": str(path),
|
||||||
|
"status": meta.get("status", ""),
|
||||||
|
"entries": entry_count(body),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_init(args) -> int:
|
||||||
|
path = memlog_path(args.workspace)
|
||||||
|
if path.exists():
|
||||||
|
print(f"error: {path} already exists; use append/set to update it", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
meta: dict[str, str] = {}
|
||||||
|
for pair in args.field or []:
|
||||||
|
if "=" not in pair:
|
||||||
|
print(f"error: --field expects key=value, got {pair!r}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
k, v = pair.split("=", 1)
|
||||||
|
meta[k.strip()] = v.strip()
|
||||||
|
meta.setdefault("status", "active")
|
||||||
|
touch(meta)
|
||||||
|
write_atomic(path, render(meta, ""))
|
||||||
|
ack(path, meta, "")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_append(args) -> int:
|
||||||
|
path = memlog_path(args.workspace)
|
||||||
|
meta, body = split(path.read_text(encoding="utf-8"))
|
||||||
|
text = " ".join(args.text.split()) # collapse newlines/runs → one-line entry, no prose bloat
|
||||||
|
tag = f"({args.type}) " if args.type else ""
|
||||||
|
entry = f"- {tag}{text}"
|
||||||
|
body = (body.rstrip("\n") + "\n" + entry) if body.strip() else entry # always at the end
|
||||||
|
touch(meta)
|
||||||
|
write_atomic(path, render(meta, body))
|
||||||
|
ack(path, meta, body)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set(args) -> int:
|
||||||
|
path = memlog_path(args.workspace)
|
||||||
|
meta, body = split(path.read_text(encoding="utf-8"))
|
||||||
|
meta[args.key] = args.value
|
||||||
|
touch(meta)
|
||||||
|
write_atomic(path, render(meta, body))
|
||||||
|
ack(path, meta, body)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
pi = sub.add_parser("init", help="create the memlog")
|
||||||
|
pi.add_argument("--workspace", required=True)
|
||||||
|
pi.add_argument("--field", action="append", metavar="KEY=VALUE", help="frontmatter field (repeatable)")
|
||||||
|
pi.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
pa = sub.add_parser("append", help="append one entry at the end")
|
||||||
|
pa.add_argument("--workspace", required=True)
|
||||||
|
pa.add_argument("--text", required=True)
|
||||||
|
pa.add_argument("--type", help="entry kind, rendered as an inline tag")
|
||||||
|
pa.set_defaults(func=cmd_append)
|
||||||
|
|
||||||
|
pset = sub.add_parser("set", help="set a frontmatter field")
|
||||||
|
pset.add_argument("--workspace", required=True)
|
||||||
|
pset.add_argument("--key", required=True)
|
||||||
|
pset.add_argument("--value", required=True)
|
||||||
|
pset.set_defaults(func=cmd_set)
|
||||||
|
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
# /// 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, type=None):
|
||||||
|
argv = ["append", "--workspace", ws, "--text", text]
|
||||||
|
if type:
|
||||||
|
argv += ["--type", type]
|
||||||
|
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", type="technique")
|
||||||
|
append(ws, "an idea", type="idea")
|
||||||
|
append(ws, "started bar", 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", type="idea")
|
||||||
|
append(ws, "how do we handle stampede?", 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", type="technique")
|
||||||
|
append(ws, "magnetic latch", type="idea")
|
||||||
|
append(ws, "started Six Hats", type="technique")
|
||||||
|
append(ws, "stale data risk", type="idea")
|
||||||
|
append(ws, "started SCAMPER", type="technique") # back to SCAMPER — just appended again
|
||||||
|
append(ws, "stackable tiers", 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_heterogeneous_entry_types_coexist(ws):
|
||||||
|
init(ws)
|
||||||
|
append(ws, "an idea", type="idea")
|
||||||
|
append(ws, "an open question", type="question")
|
||||||
|
append(ws, "a decision we made", type="decision")
|
||||||
|
append(ws, "user wants mobile-first", 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", 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", 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", type="idea")
|
||||||
|
meta, _ = memlog.split(read(ws))
|
||||||
|
assert meta["topic"] == "cars, trains, and planes"
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_emits_json_ack(ws, capsys):
|
||||||
|
init(ws)
|
||||||
|
append(ws, "x", 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
|
||||||
Loading…
Reference in New Issue