Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 513fef79c5 | |||
| afa694ba8d | |||
| 8668118f78 | |||
| e81cdc2980 | |||
| d8c9156ffd | |||
| 291a8085ef | |||
| 49041a5168 | |||
| 8cbe1634b6 | |||
| c4b651f5b0 | |||
| 33fdd3d44e | |||
| ef6c47e674 | |||
| f9c3baf5b9 | |||
| 24abf11dab | |||
| ca9a4d9c12 | |||
| aa9594d82a | |||
| 93494b8c13 | |||
| 93ed34001b | |||
| ec94feb6a4 | |||
| 831b9141a5 | |||
| 2de19713f6 | |||
| 79fcfbce6a | |||
| 3d22ba3004 | |||
| 62229d9d0a | |||
| 03ba268cb1 | |||
| de53e0ecc5 | |||
| 4128cac9ab | |||
| 9b53d7863f | |||
| a8d034b940 | |||
| 7c0937f658 | |||
| 8570d96bad | |||
| 480a2d9468 | |||
| df7452e3ba | |||
| 85a8e59d0f | |||
| 07cb7a606b | |||
| 0ef03edde6 | |||
| efe43a10ea | |||
| 4fe6fe09c8 | |||
| 8acdb49a41 | |||
| f7304bf553 | |||
| 5b034f081d | |||
| 271c883ea8 | |||
| 816ee85fb9 | |||
| 3ba3a9504d | |||
| 1667cdc000 | |||
| f916662ff3 | |||
| 0c9800cd3b | |||
| 476239d878 | |||
| 20bb19f4cd | |||
| f6565b9d23 | |||
| b6e8c636ce | |||
| 442fd1523c | |||
| ce4b5d47a0 | |||
| 59e865c079 | |||
| 96b6b21a88 | |||
| 5973f9b787 | |||
| 8887267256 | |||
| a6cfed96f1 | |||
| 2de8183e85 | |||
| 0adbc12f09 | |||
| 80ca5a07fd | |||
| 4502bd3c3b | |||
| b386502020 | |||
| 16ba55d7b4 | |||
| 5458c35b35 |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "basic-memory-local",
|
||||
"interface": {
|
||||
"displayName": "Basic Memory Local"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "codex",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Developer Tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Use when reviewing Basic Machines code for house style, architecture risk, pre-merge hardening, or whether a change fits basic-memory/basic-memory-cloud conventions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Basic Machines Review
|
||||
|
||||
Use this skill for repo-local review passes where ordinary code review needs Basic Machines
|
||||
house style and architecture judgment. Report findings only; do not edit code unless the user
|
||||
asks you to fix specific findings.
|
||||
|
||||
## Scope
|
||||
|
||||
Review the current diff or named files against:
|
||||
|
||||
- The repo's `AGENTS.md` / `CLAUDE.md`
|
||||
- `docs/ENGINEERING_STYLE.md`
|
||||
- The touched code paths and tests
|
||||
|
||||
Apply only the guidance for the active repo. In `basic-memory`, prioritize local-first
|
||||
file/database/MCP boundaries. In `basic-memory-cloud`, prioritize tenant/workspace isolation,
|
||||
cloud worker behavior, and web-v2 state/runtime boundaries.
|
||||
|
||||
## Review Rubric
|
||||
|
||||
Report only concrete, falsifiable risks:
|
||||
|
||||
- **Cognitive load:** Is the change harder to understand than the problem requires?
|
||||
- **Change propagation:** Will one product change force edits across unrelated layers?
|
||||
- **Knowledge duplication:** Is the same rule encoded in multiple places that can drift?
|
||||
- **Accidental complexity:** Did the change add abstractions, fallbacks, or state without need?
|
||||
- **Dependency direction:** Are API/MCP/CLI, services, repositories, and UI stores respecting
|
||||
their intended boundaries?
|
||||
- **Domain model distortion:** Do names and types still match the product concept, or did a
|
||||
transport/storage detail leak into the domain?
|
||||
- **Test oracle quality:** Would the tests fail for the bug or regression the change claims to
|
||||
protect against?
|
||||
|
||||
## House Rules To Check Explicitly
|
||||
|
||||
- No speculative `getattr(obj, "attr", default)` for unknown model shapes.
|
||||
- No broad exception swallowing, warning-only failure paths, or hidden fallback behavior.
|
||||
- No casts or `Any` that hide an unclear type relationship.
|
||||
- Dataclasses for internal value/result objects; Pydantic at validation/serialization
|
||||
boundaries.
|
||||
- Narrow `Protocol`s when only a capability is needed.
|
||||
- Explicit async/resource ownership, cancellation, and cleanup.
|
||||
- Meaningful regression tests or verification for risky changes.
|
||||
- Comments explain why, not what.
|
||||
|
||||
## Reporting Format
|
||||
|
||||
Lead with findings ordered by severity. Each finding should include:
|
||||
|
||||
| Severity | Use for |
|
||||
| -------- | ------- |
|
||||
| `high` | A likely correctness, security, data-loss, or tenant/workspace isolation failure |
|
||||
| `medium` | A concrete maintainability or boundary risk that can cause future defects |
|
||||
| `low` | A minor consistency issue, ambiguous guidance, or review-only cleanup |
|
||||
|
||||
```text
|
||||
severity | file:line | risk category | claim
|
||||
Why: concrete behavior or code path that proves the risk.
|
||||
Fix: smallest practical change, or "none obvious" if the risk needs product input.
|
||||
```
|
||||
|
||||
If there are no findings, say so and note any verification gaps that remain.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: fix-pr-issues
|
||||
description: Use when addressing Basic Memory pull request feedback, failed checks, or BM Bossbot blockers from Codex.
|
||||
---
|
||||
|
||||
# Fix Basic Memory PR Issues
|
||||
|
||||
Resolve PR feedback and failed checks, then wait for BM Bossbot to approve the
|
||||
new head SHA. This skill never merges a PR.
|
||||
|
||||
## Gather
|
||||
|
||||
1. Identify the PR:
|
||||
- `gh pr view --json number,url,headRefOid,mergeStateStatus,statusCheckRollup`
|
||||
|
||||
2. Collect feedback:
|
||||
- PR comments and review summaries
|
||||
- inline review comments and unresolved review threads
|
||||
- failed GitHub Actions jobs and relevant logs
|
||||
- the managed `BM_BOSSBOT_SUMMARY` block in the PR body
|
||||
|
||||
3. Build a short issue ledger:
|
||||
- source
|
||||
- concrete problem
|
||||
- expected fix
|
||||
- verification needed
|
||||
|
||||
## Fix
|
||||
|
||||
1. Address one ledger item at a time.
|
||||
2. Read each file in full before editing it.
|
||||
3. Keep diffs narrow and preserve unrelated user changes.
|
||||
4. Run the smallest meaningful verification first, then widen as needed.
|
||||
5. Commit with `git commit -s` when code or docs changed.
|
||||
|
||||
## Push And Recheck
|
||||
|
||||
1. Push the branch.
|
||||
2. Watch checks for the new `headRefOid`.
|
||||
3. Wait for the required `BM Bossbot Approval` status to pass on that exact SHA.
|
||||
4. If BM Bossbot reviews an older SHA, treat the approval as stale and keep
|
||||
waiting for the current one.
|
||||
|
||||
## Reply
|
||||
|
||||
For each addressed comment or blocker, reply with the fix commit, verification
|
||||
run, and current BM Bossbot status. Do not resolve or dismiss substantive
|
||||
feedback without evidence.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Fix PR Issues"
|
||||
short_description: "Address PR feedback and BM Bossbot blockers"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $fix-pr-issues to address PR feedback and wait for BM Bossbot Approval on the latest head SHA."
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h4l2-6 4 12 2-6h6"/>
|
||||
<path d="M4 20h16"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 249 B |
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: infographics
|
||||
description: Use when generating Basic Memory PR, changelog, release, or weekly images from Codex.
|
||||
---
|
||||
|
||||
# Basic Memory Images
|
||||
|
||||
Generate repository visuals with evidence-grounded content and canonical output
|
||||
paths. The file and marker names still say "infographic" for compatibility, but
|
||||
PR generation is image-first: scene, poster, painting, photograph, cover,
|
||||
tableau, staged artifact, or another editorial visual moment that describes the
|
||||
intent of the PR. PR images are non-gating BM Bossbot artifacts; changelog and
|
||||
release-summary images are manual evidence-pack workflows.
|
||||
|
||||
## Output Contract
|
||||
|
||||
- Base output directory: `docs/assets/infographics/`
|
||||
- PR image: `docs/assets/infographics/pr-<number>.webp`
|
||||
- Changelog image: `docs/assets/infographics/changelog.webp`
|
||||
- Weekly image:
|
||||
- This is always a 2-Week Retro window: previous ISO week through current ISO
|
||||
week (`start-week = current-week - 1`, `end-week = current-week`).
|
||||
- Same year window: `docs/assets/infographics/<year>-w<start-week>-w<end-week>.webp`
|
||||
- Cross-year window:
|
||||
`docs/assets/infographics/<start-year>-w<start-week>-<end-year>-w<end-week>.webp`
|
||||
|
||||
## PR Mode
|
||||
|
||||
PR mode uses the BM Bossbot summary block as source material. Do not hand-write
|
||||
claims that are not present in the PR body.
|
||||
|
||||
1. Fetch the PR body:
|
||||
|
||||
```bash
|
||||
gh pr view <number> --json body --jq '.body // ""' > /tmp/bm-pr-body.md
|
||||
```
|
||||
|
||||
2. Generate the canonical asset:
|
||||
|
||||
```bash
|
||||
uv run --script scripts/generate_pr_infographic.py \
|
||||
--pr-number <number> \
|
||||
--pr-body-file /tmp/bm-pr-body.md \
|
||||
--theme "<optional visual theme>" \
|
||||
--provenance-output /tmp/bm-infographic-provenance.md \
|
||||
--output docs/assets/infographics/pr-<number>.webp
|
||||
```
|
||||
|
||||
If the PR body contains a managed image theme block, the script reads it
|
||||
automatically:
|
||||
|
||||
```markdown
|
||||
<!-- BM_INFOGRAPHIC_THEME:start -->
|
||||
<theme>
|
||||
<!-- BM_INFOGRAPHIC_THEME:end -->
|
||||
```
|
||||
|
||||
Before spending an image call, test the prompt path locally:
|
||||
|
||||
```bash
|
||||
uv run --script scripts/generate_pr_infographic.py \
|
||||
--pr-number <number> \
|
||||
--pr-body-file /tmp/bm-pr-body.md \
|
||||
--theme "<optional visual theme>" \
|
||||
--output docs/assets/infographics/pr-<number>.webp \
|
||||
--print-prompt
|
||||
```
|
||||
|
||||
`--dry-run` is an alias for `--print-prompt`; both print the final prompt and
|
||||
exit without calling OpenAI.
|
||||
|
||||
When no theme is supplied, the script selects a deterministic BM visual
|
||||
direction from the style pool below based on the PR number and Bossbot summary.
|
||||
This keeps repeated PR images from collapsing into the same generic visual.
|
||||
|
||||
When the image is generated, also write provenance with
|
||||
`--provenance-output <path>`. BM Bossbot publishes that managed block into the
|
||||
PR body with these markers:
|
||||
|
||||
```markdown
|
||||
<!-- BM_INFOGRAPHIC_PROVENANCE:start -->
|
||||
...
|
||||
<!-- BM_INFOGRAPHIC_PROVENANCE:end -->
|
||||
```
|
||||
|
||||
The provenance block records the generated asset path, image model, size,
|
||||
quality, image mode, theme source, and selected visual direction. It
|
||||
intentionally does not dump the full generated prompt into the PR body. Treat
|
||||
this block as debugging and creative provenance only; it is not a merge gate.
|
||||
|
||||
The PR image is visual support only. The authoritative merge gate is the
|
||||
GitHub commit status named `BM Bossbot Approval`.
|
||||
|
||||
## Changelog Mode
|
||||
|
||||
Build an evidence pack before writing a prompt:
|
||||
|
||||
- diff truth source: merged PR diffs, merge commits, or local reconstructed diffs
|
||||
- changed-file orientation: `git diff --stat` plus key file reads
|
||||
- impact ledger: before/after outcomes tied to actual changes
|
||||
- discard list: misleading titles, reverted work, rename-only churn, speculative TODOs
|
||||
- chosen image form: poster, scene, tableau, cover, painting, photograph,
|
||||
staged artifact, or another editorial visual moment
|
||||
- chosen BM style category: exactly one category from the selection pool below
|
||||
|
||||
Read these references before drafting the prompt:
|
||||
|
||||
- `references/prompt-blueprint.md`
|
||||
- `references/style-balance.md`
|
||||
|
||||
Read the current `CHANGELOG.md` entries and include the latest meaningful
|
||||
changes.
|
||||
|
||||
## Style And Category Selection
|
||||
|
||||
Select exactly one BM style category per image based on semantic fit. The
|
||||
visual language should be recognizable and tasteful, while staying
|
||||
business-readable.
|
||||
|
||||
Create an image-first visual form that communicates the change: poster, scene,
|
||||
tableau, cover image, painting, photograph, staged artifact, or another
|
||||
editorial visual moment. Maps, diagrams, dossiers, charts, and labels can appear
|
||||
as props inside the scene, but do not make a text-heavy infographic.
|
||||
|
||||
BM category pool:
|
||||
|
||||
- computer science college textbooks: SICP-style diagrams, algorithms lectures,
|
||||
compiler pipelines, automata, database systems, type theory, operating systems
|
||||
- classic literature subjects: sea voyages, gothic manors, Dickensian city maps,
|
||||
Austen social graphs, library marginalia, travel journals
|
||||
- fantasy/D&D-inspired: quest maps, dungeon keys, guild ledgers, spellbooks,
|
||||
bestiaries, tavern notice boards; no copyrighted settings
|
||||
- Music: Metal, Hard Rock, Punk, techno, soul, reggae bands; no pop music, no
|
||||
direct band logos, album covers, or musician likenesses
|
||||
- sci-fi: Star Wars inspired knockoff, Spaceballs-adjacent space opera, fleet
|
||||
routes, mission consoles, contraband manifests; avoid copyrighted characters,
|
||||
logos, or named fictional universes
|
||||
- Conan the barbarian-inspired sword-and-sorcery: ruined temples, desert routes,
|
||||
battle standards, ancient maps; no named character likenesses
|
||||
- Comic books: issue covers, splash pages, action-panel maps, caption boxes,
|
||||
halftone energy, clean sound-effect typography
|
||||
- French new wave movies: poster style, stark typography, city route maps,
|
||||
jump-cut sequencing, high-contrast editorial photography cues
|
||||
- WWII propaganda posters: home-front public-information poster language,
|
||||
logistics arrows, ration charts, mobilization maps, bold simplified figures;
|
||||
no real-world party symbols, hate imagery, dehumanizing slogans, or false
|
||||
historical claims
|
||||
- Italian movie posters: hand-painted drama, bold credits, expressive color,
|
||||
route-map collage, 1960s or 1970s cinema energy; no direct film titles or
|
||||
actor likenesses
|
||||
- Shakespeare: stage maps, acts and scenes, dramatis personae, royal courts,
|
||||
backstage cue sheets
|
||||
- Greek mythology: temple diagrams, constellation routes, hero's journey maps,
|
||||
oracle tablets, labyrinths, ship routes
|
||||
- noir detective boards: case files, red-string maps, typed evidence labels,
|
||||
precinct wall charts
|
||||
- NASA mission-control dashboards: launch timelines, telemetry maps, orbital
|
||||
routes, status boards
|
||||
- space exploration and astronomy: celestial atlases, observatory charts,
|
||||
star-field maps, orbital mechanics diagrams, planetary survey routes,
|
||||
telescope annotations, mission trajectories, deep-space timelines
|
||||
- paintings: abstract painting, classical landscape, Remington-inspired western
|
||||
action painting, Rembrandt-inspired chiaroscuro, historical mural, stormy
|
||||
seascape, allegorical editorial painting
|
||||
- classic black-and-white photography: documentary field report, newsroom
|
||||
archive print, editorial photo essay, street photography, high-contrast
|
||||
darkroom print, contact sheet, civic infrastructure photograph
|
||||
- 80's action movies: practical explosions, smoky backlit warehouses, neon city
|
||||
streets, helicopter searchlights, mission dossiers, heroic silhouettes,
|
||||
high-stakes countdowns, painted ensemble posters; no direct actor likenesses,
|
||||
real film titles, franchise marks, or catchphrases
|
||||
- alchemy manuscripts: transformation diagrams, annotated symbols, recipe-like
|
||||
process maps, illuminated margins
|
||||
- brutalist civic planning: transit maps, concrete signage, zoning blocks,
|
||||
infrastructure diagrams
|
||||
|
||||
Selection rules:
|
||||
|
||||
- Pick one category only; do not create mixed mashups.
|
||||
- Pick the most appropriate image form. Prefer an actual scene, poster,
|
||||
painting, photograph, tableau, or cover over a text-heavy infographic.
|
||||
- Match metaphor to content, but do not overthink it. The category is a creative
|
||||
catalyst, not a semantic constraint.
|
||||
- Use a polished editorial rendering direction: smooth anti-aliased
|
||||
text, high contrast, clean edges, readable labels.
|
||||
- Make the category drive the composition through a readable staged moment,
|
||||
editorial composition, symbolic environment, route, artifact, or visual
|
||||
metaphor.
|
||||
- Keep the structure literal enough to aid understanding, but not so heavy that
|
||||
it obscures engineering meaning.
|
||||
- Give the image generator creative latitude on layout, structure, color palette,
|
||||
and visual metaphors. Be precise about what content to show, loose about how
|
||||
to show it.
|
||||
- Do not use copyrighted characters, logos, or named fictional universes. Use
|
||||
genre cues, knockoffs, and original compositions instead.
|
||||
|
||||
## Content-First Aesthetic Contract
|
||||
|
||||
The meaning must be readable and clearly hierarchical. Everything else is
|
||||
creative territory: image form, layout, visual metaphors, decorative elements,
|
||||
color choices, and category-specific visual language.
|
||||
|
||||
Hierarchy:
|
||||
|
||||
1. Meaning: what shipped, what changed, and why it matters must be clear.
|
||||
2. If the image uses text, labels, sections, or evidence bullets, they must be
|
||||
legible.
|
||||
3. The selected category's visual DNA should drive the composition as a poster,
|
||||
scene, painting, photograph, tableau, cover, or symbolic object arrangement.
|
||||
4. Do not play it safe. A visually striking image that someone wants to look at
|
||||
beats a correct but boring one.
|
||||
|
||||
Hard rules:
|
||||
|
||||
- Content sections and labels must be readable when present. Text cannot be
|
||||
obscured by decorations.
|
||||
- Do not use lore-heavy copy that competes with engineering or business meaning.
|
||||
- Every prompt must include a clear image-first composition cue: a staged scene,
|
||||
poster composition, painting, photograph, symbolic tableau, hero object,
|
||||
mission room, dossier, artifact, route, or visual metaphor.
|
||||
- Do not over-prescribe exact coordinates or panel geometry; give a composition
|
||||
backbone and let the model compose around it.
|
||||
|
||||
## Generation
|
||||
|
||||
1. Write the final prompt to a temporary markdown file.
|
||||
2. Generate with the shared image helper:
|
||||
|
||||
```bash
|
||||
uv run --script scripts/generate_infographic.py \
|
||||
--prompt-file /tmp/bm-infographic-prompt.md \
|
||||
--output docs/assets/infographics/<name>.webp
|
||||
```
|
||||
|
||||
3. Verify the image exists and is readable before reporting success.
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- Tell a concrete before/after value story, not vague improvement claims.
|
||||
- Stay understandable for both engineers and non-technical stakeholders.
|
||||
- Use plain-language section titles and labels when text is present.
|
||||
- Include clear visual hierarchy: title, staged focal point, symbolic scene,
|
||||
evidence props, or hero object.
|
||||
- Avoid invented facts; only use provided source material.
|
||||
- Favor shipped outcomes over intermediate or reverted work.
|
||||
- Preserve readability with high contrast, non-tiny labels, and uncluttered
|
||||
layout.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Infographics"
|
||||
short_description: "Generate Basic Memory repo infographics"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $infographics to generate a Basic Memory PR or changelog infographic with canonical output paths."
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h4l2-6 4 12 2-6h6"/>
|
||||
<path d="M4 20h16"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 249 B |
@@ -0,0 +1,72 @@
|
||||
# Prompt Blueprint
|
||||
|
||||
Convert an evidence pack into a final visual prompt. Be precise about the
|
||||
content and loose about visual execution.
|
||||
|
||||
## Required Inputs
|
||||
|
||||
- Diff truth source summary
|
||||
- Changed-file orientation summary
|
||||
- Impact ledger with before/after outcomes
|
||||
- Discard list for excluded noise
|
||||
- Chosen image form
|
||||
- Chosen BM style category
|
||||
|
||||
## Prompt Shape
|
||||
|
||||
```text
|
||||
Create a polished Basic Memory editorial image inspired by
|
||||
<BM_STYLE_CATEGORY>. Use a poster, scene, tableau, painting, photograph, cover
|
||||
image, staged artifact, or another image-first form that best communicates the
|
||||
intent. Use HD editorial rendering with smooth anti-aliased text when text is
|
||||
present. Go bold and let the selected category drive the visual language through
|
||||
original, non-infringing cues.
|
||||
|
||||
TITLE:
|
||||
- "<clear title>"
|
||||
- "<scope subtitle>"
|
||||
|
||||
COMPOSITION:
|
||||
- Recreate a clear staged moment or symbolic image that describes the PR
|
||||
intent.
|
||||
- Maps, diagrams, dossiers, route lines, labels, and artifacts can appear as
|
||||
props inside the scene, but the output should read as an image rather than a
|
||||
dense infographic.
|
||||
- Take creative liberty with layout and styling.
|
||||
- The hard rule: the meaning must be readable and clearly hierarchical.
|
||||
- Keep labels plain-language and technical when labels are used.
|
||||
|
||||
CONTENT:
|
||||
1. "<section>"
|
||||
- <evidence-grounded outcome>
|
||||
- <evidence-grounded outcome>
|
||||
|
||||
2. "<section>"
|
||||
- <evidence-grounded outcome>
|
||||
- <evidence-grounded outcome>
|
||||
|
||||
METRICS:
|
||||
- <metric>
|
||||
- <metric>
|
||||
|
||||
STYLE DIRECTION:
|
||||
- Upscaled editorial, high contrast, anti-aliased text, smooth edges.
|
||||
- Let the category's visual DNA drive the composition.
|
||||
- Use genre/category cues only; do not use copyrighted characters, logos, named
|
||||
fictional universes, direct band logos, album art, or celebrity likenesses.
|
||||
|
||||
DO NOT:
|
||||
- Make text unreadable or let decoration obscure content.
|
||||
- Render a text-heavy infographic, dashboard, flowchart, timeline strip,
|
||||
checklist, bullet-list panel, or dense explanatory diagram.
|
||||
- Use crunchy low-resolution pixel art.
|
||||
- Invent facts not present in the evidence pack.
|
||||
```
|
||||
|
||||
## Writing Rules
|
||||
|
||||
- Keep each bullet specific and evidence-grounded.
|
||||
- Prefer outcome language over implementation trivia.
|
||||
- Default to three or four sections; never exceed five.
|
||||
- Give proportionally more space to dominant changes.
|
||||
- Keep the final prompt short, energetic, and readable.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Style Balance Rubric
|
||||
|
||||
## Core Principle
|
||||
|
||||
Be bold, not confusing. The selected BM style category should structure the
|
||||
visual through a readable image-first composition, not decorate a generic grid.
|
||||
Use an editorial scene, poster, painting, photograph, cover, staged artifact, or
|
||||
tableau that turns the PR intent into a visual moment.
|
||||
|
||||
## Required Traits
|
||||
|
||||
- Anti-aliased typography
|
||||
- Smooth edges
|
||||
- High contrast between text and background
|
||||
- Plain-language section labels
|
||||
- Clear composition backbone: staged scene, editorial poster, painting,
|
||||
photograph, symbolic tableau, hero artifact, dossier, mission room, or route
|
||||
embodied as part of the scene
|
||||
- A single coherent BM style category, expressed through original visual cues
|
||||
|
||||
## Reject Or Rewrite If
|
||||
|
||||
- Content text is unreadable.
|
||||
- The prompt lacks a composition backbone.
|
||||
- The prompt over-prescribes exact panel positions or a rigid grid.
|
||||
- The style leans into crunchy low-resolution pixelation.
|
||||
- Copy uses lore-heavy references instead of engineering meaning.
|
||||
- The prompt uses copyrighted characters, logos, named fictional universes,
|
||||
direct band logos, album art, or celebrity likenesses.
|
||||
|
||||
## Creative Integration Patterns
|
||||
|
||||
- Use category-native map details to organize content: textbook diagrams,
|
||||
literary journeys, quest maps, tour posters, mission-control routes, stage
|
||||
blocking, mythic constellations, star charts, mission trajectories, case
|
||||
boards, or civic plans as props inside the image.
|
||||
- Recreate a scene, editorial poster, painting, photograph, cover, artifact, or
|
||||
tableau instead of sectioned bullets.
|
||||
- Map engineering metrics to visual counters, route progress, or status boards
|
||||
only when they naturally belong in the scene.
|
||||
- Let headers and accents borrow from the selected style.
|
||||
- Keep atmospheric details behind or around content, never over it.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: pr-create
|
||||
description: Use when creating or updating a Basic Memory pull request from Codex with BM Bossbot merge-gate monitoring.
|
||||
---
|
||||
|
||||
# Create A Basic Memory PR
|
||||
|
||||
Create or update a pull request for the current branch, then wait for BM
|
||||
Bossbot to approve the latest head SHA. This skill never merges a PR.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Optional `<theme>`: free-form visual direction for the non-gating PR
|
||||
image. Example: `$pr-create "Italian movie poster"`.
|
||||
- Treat `<theme>` as style guidance only. It must not affect PR readiness,
|
||||
BM Bossbot review, status checks, or merge behavior.
|
||||
|
||||
## How To Use
|
||||
|
||||
Ask Codex to use the skill from a feature branch:
|
||||
|
||||
```text
|
||||
$pr-create
|
||||
$pr-create "Italian movie poster"
|
||||
$pr-create "80's action movies"
|
||||
```
|
||||
|
||||
Use the plain form when you only want the PR workflow. Pass a theme when you
|
||||
want the non-gating image to lean toward a particular visual direction. The
|
||||
theme can be specific ("Rembrandt-inspired approval scene") or broad ("let the
|
||||
model choose from BM categories").
|
||||
|
||||
## What Happens
|
||||
|
||||
1. Codex checks the branch, local verification, GitHub auth, commit sign-offs,
|
||||
and semantic PR title shape.
|
||||
2. Codex pushes the branch, creates or reuses the PR, and adds the optional
|
||||
`BM_INFOGRAPHIC_THEME` block when a theme was supplied.
|
||||
3. BM Bossbot runs from trusted base code, reviews sanitized PR metadata and
|
||||
diff context, and sets the required `BM Bossbot Approval` status for the
|
||||
exact head SHA.
|
||||
4. If approval succeeds, BM Bossbot may publish a non-gating image block and a
|
||||
provenance block:
|
||||
|
||||
```markdown
|
||||
<!-- BM_INFOGRAPHIC_PROVENANCE:start -->
|
||||
...
|
||||
<!-- BM_INFOGRAPHIC_PROVENANCE:end -->
|
||||
```
|
||||
|
||||
The provenance records the image mode, theme source, selected visual
|
||||
direction, and image settings. It is for review/debugging context only.
|
||||
5. Codex reports the PR URL, head SHA, checks watched, verification run, and BM
|
||||
Bossbot verdict.
|
||||
|
||||
The skill never merges, never enables auto-merge, and never treats the image or
|
||||
provenance block as a gate. The only required merge signal is the
|
||||
`BM Bossbot Approval` status on the current PR head SHA.
|
||||
|
||||
## Preflight
|
||||
|
||||
1. Confirm the repo and branch:
|
||||
- `git status --short --branch`
|
||||
- stop if detached or on `main`
|
||||
- keep unrelated user changes intact
|
||||
|
||||
2. Confirm GitHub access:
|
||||
- `gh auth status`
|
||||
- `gh repo view --json nameWithOwner,defaultBranchRef,url`
|
||||
|
||||
3. Check PR readiness:
|
||||
- commits are signed off with `git commit -s`
|
||||
- title uses the repo semantic format
|
||||
- local verification appropriate to the change has run
|
||||
|
||||
## Create Or Reuse
|
||||
|
||||
1. Push the branch:
|
||||
- `git push -u origin HEAD`
|
||||
|
||||
2. Check for an existing PR:
|
||||
- `gh pr view --json number,url,headRefOid,mergeStateStatus,statusCheckRollup`
|
||||
|
||||
3. If no PR exists, create one:
|
||||
- `gh pr create --fill`
|
||||
- adjust the title if it does not satisfy the semantic PR title workflow
|
||||
|
||||
4. If `<theme>` is provided, add or update this managed block in the PR body:
|
||||
|
||||
```markdown
|
||||
<!-- BM_INFOGRAPHIC_THEME:start -->
|
||||
<theme>
|
||||
<!-- BM_INFOGRAPHIC_THEME:end -->
|
||||
```
|
||||
|
||||
Keep the rest of the PR body intact. The theme is non-gating image guidance
|
||||
only.
|
||||
|
||||
5. Do not merge. Do not enable auto-merge.
|
||||
|
||||
## Watch The Gate
|
||||
|
||||
1. Trigger or wait for `.github/workflows/bm-bossbot.yml`.
|
||||
2. Watch the required commit status named `BM Bossbot Approval`.
|
||||
3. Treat approval as valid only when it is green for the current `headRefOid`.
|
||||
4. If the branch changes after approval, wait for BM Bossbot to review the new
|
||||
head SHA.
|
||||
5. If BM Bossbot fails or requests changes, use `$fix-pr-issues`.
|
||||
|
||||
## Report
|
||||
|
||||
Return the PR URL, current head SHA, checks watched, verification run, and the
|
||||
BM Bossbot verdict. Include the image `<theme>` if one was supplied. Be
|
||||
explicit when any check is still pending.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "PR Create"
|
||||
short_description: "Create PRs and wait for BM Bossbot"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $pr-create to create or update this Basic Memory PR and wait for BM Bossbot Approval."
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h4l2-6 4 12 2-6h6"/>
|
||||
<path d="M4 20h16"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 249 B |
@@ -6,18 +6,24 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official Basic Memory plugins from the canonical basic-memory repository",
|
||||
"version": "0.3.13"
|
||||
"version": "0.21.6"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "basic-memory",
|
||||
"source": "./plugins/claude-code",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph — session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.3.13",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph \u2014 session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.21.6",
|
||||
"author": {
|
||||
"name": "Basic Machines"
|
||||
},
|
||||
"keywords": ["memory", "knowledge", "mcp", "specs", "context"]
|
||||
"keywords": [
|
||||
"memory",
|
||||
"knowledge",
|
||||
"mcp",
|
||||
"specs",
|
||||
"context"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ The justfile target handles:
|
||||
- ✅ Beta version format validation (supports b1, b2, rc1, etc.)
|
||||
- ✅ Git status and branch checks
|
||||
- ✅ Quality checks (`just check` - lint, format, type-check, tests)
|
||||
- ✅ Version update in `src/basic_memory/__init__.py`
|
||||
- ✅ Version update across all consolidated manifests via `just set-version` (Python
|
||||
package + Claude Code plugin/marketplaces + Codex plugin + Hermes + OpenClaw)
|
||||
- ✅ Automatic commit with proper message
|
||||
- ✅ Tag creation and pushing to GitHub
|
||||
- ✅ Beta release workflow trigger
|
||||
@@ -90,6 +91,6 @@ Monitor release: https://github.com/basicmachines-co/basic-memory/actions
|
||||
- Beta releases are pre-releases for testing new features
|
||||
- Automatically published to PyPI with pre-release flag
|
||||
- Uses the automated justfile target for consistency
|
||||
- Version is automatically updated in `__init__.py`
|
||||
- Version is automatically updated across all consolidated manifests via `just set-version`
|
||||
- Ideal for validating changes before stable release
|
||||
- Supports both beta (b1, b2) and release candidate (rc1, rc2) versions
|
||||
- Supports both beta (b1, b2) and release candidate (rc1, rc2) versions
|
||||
|
||||
@@ -42,7 +42,8 @@ The justfile target handles:
|
||||
- ✅ Version format validation
|
||||
- ✅ Git status and branch checks
|
||||
- ✅ Quality checks (`just check` - lint, format, type-check, tests)
|
||||
- ✅ Version update across all consolidated manifests via `just set-version` (Python package + Claude Code plugin/marketplaces + Hermes + OpenClaw)
|
||||
- ✅ Version update across all consolidated manifests via `just set-version` (Python
|
||||
package + Claude Code plugin/marketplaces + Codex plugin + Hermes + OpenClaw)
|
||||
- ✅ Automatic commit with proper message
|
||||
- ✅ Tag creation and pushing to GitHub
|
||||
- ✅ Release workflow trigger (automatic on tag push)
|
||||
@@ -194,12 +195,13 @@ Users can now upgrade:
|
||||
- Version is automatically updated across **all** consolidated manifests via
|
||||
`just set-version <version>` (which calls `scripts/update_versions.py`): the
|
||||
Python package (`__init__.py`, `server.json`) **and** the plugin/agent artifacts
|
||||
(Claude Code `plugin.json` + root/local marketplaces, Hermes `plugin.yaml` +
|
||||
`__init__.py`, OpenClaw `package.json`). To bump only the plugin/agent artifacts
|
||||
(Claude Code `plugin.json` + root/local marketplaces, Codex `plugin.json`,
|
||||
Hermes `plugin.yaml` + `__init__.py`, OpenClaw `package.json`). To bump only
|
||||
the plugin/agent artifacts
|
||||
out of band, use `just set-packages-version <version>` (preview with
|
||||
`just set-packages-version-dry-run <version>`).
|
||||
- Triggers automated GitHub release with changelog
|
||||
- Package is published to PyPI for `pip` and `uv` users
|
||||
- Homebrew formula is automatically updated for stable releases
|
||||
- MCP Registry is updated manually via `mcp-publisher publish`
|
||||
- Supports multiple installation methods (uv, pip, Homebrew)
|
||||
- Supports multiple installation methods (uv, pip, Homebrew)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/basic-machines-review
|
||||
@@ -0,0 +1,25 @@
|
||||
# Auto BM Soul
|
||||
|
||||
Write project updates for humans who will return later trying to understand what happened.
|
||||
|
||||
## Voice
|
||||
|
||||
- Clear, direct, warm, and technically honest.
|
||||
- Prefer concrete observations over generic praise.
|
||||
- It is okay to say when code is messy, risky, clever, boring, or satisfying.
|
||||
- Keep personality in service of memory, not performance.
|
||||
|
||||
## Do
|
||||
|
||||
- Tell the story.
|
||||
- Name the tradeoffs.
|
||||
- Call out sharp edges.
|
||||
- Notice good simplifications.
|
||||
- Let the note have taste and a little life when the evidence supports it.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Do not invent intent, impact, tests, or drama.
|
||||
- Dunk on people.
|
||||
- Turn the note into marketing copy.
|
||||
- Hide uncertainty behind confident prose.
|
||||
@@ -0,0 +1,7 @@
|
||||
project: dev
|
||||
workspace: basic-memory-7020de4e925843c68c9056c60d101d9e
|
||||
deploy_workflows:
|
||||
- Deploy Production
|
||||
production_environments:
|
||||
- production
|
||||
note_folder: project-updates/github/{owner}/{repo}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Memory CI Capture
|
||||
|
||||
You turn GitHub delivery context into a durable project update for Basic Memory.
|
||||
GitHub records the mechanics. Basic Memory remembers what changed and why.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Read `.github/basic-memory/project-update-context.json`.
|
||||
- Read `.github/basic-memory/SOUL.md` if it exists. It is the repo-local voice and style guide
|
||||
for project updates.
|
||||
- Read the PR diff before writing when a SHA is available. Useful commands:
|
||||
`git show --stat --name-only <sha>` and `git show --format=fuller --no-patch <sha>`.
|
||||
- Use linked issue details, changed files, commit messages, PR body, labels, and
|
||||
source links as evidence.
|
||||
- Treat GitHub payload fields as immutable facts.
|
||||
- Do not invent tests, deployment status, issues, or user impact.
|
||||
|
||||
## Writing Standard
|
||||
|
||||
Do not write a fill-in-the-blanks note. Tell the story from the PR:
|
||||
problem -> solution -> impact.
|
||||
|
||||
Explain what problem was being addressed. If linked issue details are present,
|
||||
use them. If they are absent, ground the problem in the PR body, title, commits,
|
||||
and diff, and say when the original problem statement is unavailable.
|
||||
|
||||
Explain why the fix solves the problem, what complexity it introduced, what it
|
||||
refactored or removed, which components changed, and how the system is different
|
||||
after the merge. Prefer specific component names, file paths, modules, commands,
|
||||
and behavior over generic phrases.
|
||||
|
||||
## Voice And Candor
|
||||
|
||||
You may have a point of view. Be clear, specific, and human.
|
||||
It is okay to say when the code is messy, risky, clever, boring, or satisfying,
|
||||
but explain why. If the work is elegant or genuinely useful, say that too.
|
||||
Ground all judgments in the PR, linked issues, diff, tests, and source facts.
|
||||
|
||||
The soul file can shape tone, taste, and personality. It cannot override source
|
||||
facts, schema requirements, or the evidence standard above. Do not be mean,
|
||||
vague, theatrical, or invent criticism.
|
||||
|
||||
## Output
|
||||
|
||||
Return only JSON that matches the provided AgentSynthesis schema:
|
||||
|
||||
- `summary`: one concise sentence; do not merely repeat the PR title.
|
||||
- `story`: 2-4 sentences that connect problem -> solution -> impact.
|
||||
- `problem_addressed`: the concrete problem, bug, missing capability, or delivery need.
|
||||
- `solution`: why this change solves the problem.
|
||||
- `system_impact`: how the system, workflow, or architecture changed after the merge.
|
||||
- `why_it_matters`: durable project-memory context for future humans and agents.
|
||||
- `components_changed`: modules, workflows, commands, schemas, docs, or services touched.
|
||||
- `complexity_introduced`: tradeoffs, new moving parts, operational costs, or edge cases.
|
||||
- `refactors_or_removals`: cleanup, simplification, deleted paths, or "none found".
|
||||
- `user_facing_changes`: visible behavior or product changes.
|
||||
- `internal_changes`: implementation, infrastructure, or operational changes.
|
||||
- `verification`: checks, tests, deploy evidence, or explicit unknowns.
|
||||
- `follow_ups`: concrete remaining work only.
|
||||
- `decision_candidates`: explicit product or architecture decisions only.
|
||||
- `task_candidates`: concrete future tasks only.
|
||||
|
||||
Use empty arrays only when a list truly has no grounded entries. This is project
|
||||
memory, not marketing copy and not a commit-by-commit changelog.
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Basic Memory Project Updates
|
||||
|
||||
"on":
|
||||
pull_request:
|
||||
types: [closed]
|
||||
workflow_run:
|
||||
workflows: ["Deploy Production"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
project-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Basic Memory from checkout
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
|
||||
- name: Collect project update context
|
||||
id: collect
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
bm ci collect \
|
||||
--config .github/basic-memory/config.yml \
|
||||
--output .github/basic-memory/project-update-context.json
|
||||
|
||||
- name: Stop when event is not eligible
|
||||
if: steps.collect.outputs.eligible != 'true'
|
||||
run: |
|
||||
echo "Auto BM skipped: ${{ steps.collect.outputs.skip_reason }}"
|
||||
|
||||
- name: Write Codex output schema
|
||||
if: steps.collect.outputs.eligible == 'true'
|
||||
run: |
|
||||
bm ci agent-schema --output "${{ runner.temp }}/agent-synthesis.schema.json"
|
||||
|
||||
- name: Synthesize project update with Codex
|
||||
if: steps.collect.outputs.eligible == 'true'
|
||||
uses: openai/codex-action@v1
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/basic-memory/memory-ci-capture.md
|
||||
output-file: ${{ runner.temp }}/agent-synthesis.json
|
||||
output-schema-file: ${{ runner.temp }}/agent-synthesis.schema.json
|
||||
sandbox: read-only
|
||||
safety-strategy: drop-sudo
|
||||
|
||||
- name: Publish project update
|
||||
if: steps.collect.outputs.eligible == 'true'
|
||||
env:
|
||||
BASIC_MEMORY_CLOUD_API_KEY: ${{ secrets.BASIC_MEMORY_API_KEY }}
|
||||
BASIC_MEMORY_CI_CLOUD_HOST: ${{ vars.BASIC_MEMORY_CLOUD_HOST }}
|
||||
run: |
|
||||
if [ -n "$BASIC_MEMORY_CI_CLOUD_HOST" ]; then
|
||||
export BASIC_MEMORY_CLOUD_HOST="$BASIC_MEMORY_CI_CLOUD_HOST"
|
||||
fi
|
||||
bm ci publish \
|
||||
--cloud \
|
||||
--config .github/basic-memory/config.yml \
|
||||
--context .github/basic-memory/project-update-context.json \
|
||||
--synthesis "${{ runner.temp }}/agent-synthesis.json"
|
||||
@@ -0,0 +1,193 @@
|
||||
name: BM Bossbot
|
||||
|
||||
"on":
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Tests
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to review
|
||||
required: true
|
||||
# Review-thread activity re-evaluates the approval status without re-running
|
||||
# the full LLM review: new feedback flips the gate to failure, resolving the
|
||||
# last thread restores a previously earned approval for the same head SHA.
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
pull_request_review_comment:
|
||||
types:
|
||||
- created
|
||||
pull_request_review_thread:
|
||||
types:
|
||||
- resolved
|
||||
- unresolved
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
issues: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
BM_BOSSBOT_STATUS_CONTEXT: "BM Bossbot Approval"
|
||||
|
||||
jobs:
|
||||
review:
|
||||
name: BM Bossbot Review
|
||||
# Job-level concurrency (not workflow-level): thread-recheck events for the
|
||||
# same PR must never cancel an in-flight review run, and vice versa.
|
||||
concurrency:
|
||||
group: bm-bossbot-review-${{ github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.pull_requests[0].number != ''
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
pr_number: ${{ steps.pr.outputs.pr_number }}
|
||||
should_review: ${{ steps.pr.outputs.should_review }}
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted base ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Normalize PR event
|
||||
id: pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
event_file="${RUNNER_TEMP}/bm-bossbot-event.json"
|
||||
should_review=true
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
pr_number="${{ inputs.pr_number }}"
|
||||
tested_sha=""
|
||||
else
|
||||
pr_number="$(jq -r '.workflow_run.pull_requests[0].number // ""' "${GITHUB_EVENT_PATH}")"
|
||||
tested_sha="$(jq -r '.workflow_run.head_sha // ""' "${GITHUB_EVENT_PATH}")"
|
||||
fi
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" > "${RUNNER_TEMP}/pull.json"
|
||||
current_head_sha="$(jq -r '.head.sha' "${RUNNER_TEMP}/pull.json")"
|
||||
draft="$(jq -r '.draft' "${RUNNER_TEMP}/pull.json")"
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
tests_run_id="$(
|
||||
gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/test.yml/runs" \
|
||||
-f event=push \
|
||||
-f head_sha="${current_head_sha}" \
|
||||
-f status=completed \
|
||||
--jq '[.workflow_runs[] | select(.conclusion == "success")][0].id // ""'
|
||||
)"
|
||||
if [ -z "${tests_run_id}" ]; then
|
||||
should_review=false
|
||||
echo "BM Bossbot skipped PR ${pr_number}: no successful Tests workflow for ${current_head_sha}."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${draft}" = "true" ]; then
|
||||
should_review=false
|
||||
echo "BM Bossbot skipped PR ${pr_number}: draft pull request."
|
||||
fi
|
||||
|
||||
if [ -n "${tested_sha}" ] && [ "${tested_sha}" != "${current_head_sha}" ]; then
|
||||
should_review=false
|
||||
echo "BM Bossbot skipped PR ${pr_number}: Tests passed for ${tested_sha}, but current head is ${current_head_sha}."
|
||||
fi
|
||||
|
||||
jq --arg repo "${GITHUB_REPOSITORY}" \
|
||||
'{repository:{full_name:$repo}, pull_request:{number:.number,title:.title,body:(.body // ""),html_url:.html_url,head:{sha:.head.sha,ref:.head.ref},base:{ref:.base.ref,sha:.base.sha},author_association:.author_association,draft:.draft}}' \
|
||||
"${RUNNER_TEMP}/pull.json" > "${event_file}"
|
||||
|
||||
echo "event_file=${event_file}" >> "${GITHUB_OUTPUT}"
|
||||
echo "pr_number=$(jq -r '.pull_request.number' "${event_file}")" >> "${GITHUB_OUTPUT}"
|
||||
echo "head_sha=$(jq -r '.pull_request.head.sha' "${event_file}")" >> "${GITHUB_OUTPUT}"
|
||||
echo "head_ref=$(jq -r '.pull_request.head.ref' "${event_file}")" >> "${GITHUB_OUTPUT}"
|
||||
echo "author_association=$(jq -r '.pull_request.author_association // ""' "${event_file}")" >> "${GITHUB_OUTPUT}"
|
||||
echo "tested_sha=${tested_sha}" >> "${GITHUB_OUTPUT}"
|
||||
echo "should_review=${should_review}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Classify PR author
|
||||
id: trust
|
||||
if: steps.pr.outputs.should_review == 'true'
|
||||
env:
|
||||
AUTHOR_ASSOCIATION: ${{ steps.pr.outputs.author_association }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${AUTHOR_ASSOCIATION}" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
trusted_author=true
|
||||
;;
|
||||
*)
|
||||
trusted_author=false
|
||||
;;
|
||||
esac
|
||||
echo "trusted_author=${trusted_author}" >> "${GITHUB_OUTPUT}"
|
||||
echo "author_association=${AUTHOR_ASSOCIATION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Mark BM Bossbot approval pending
|
||||
if: steps.pr.outputs.should_review == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
uv run --script scripts/bm_bossbot_status.py pending \
|
||||
--event "${{ steps.pr.outputs.event_file }}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Finalize BM Bossbot approval
|
||||
if: always() && steps.pr.outputs.should_review == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
uv run --script scripts/bm_bossbot_status.py finalize \
|
||||
--event "${{ steps.pr.outputs.event_file }}" \
|
||||
--trusted "${{ steps.trust.outputs.trusted_author }}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
recheck:
|
||||
name: BM Bossbot Thread Recheck
|
||||
if: |
|
||||
github.event_name == 'pull_request_review' ||
|
||||
github.event_name == 'pull_request_review_comment' ||
|
||||
github.event_name == 'pull_request_review_thread'
|
||||
runs-on: ubuntu-latest
|
||||
# Job-level concurrency: collapse bursts of thread events for one PR while
|
||||
# staying isolated from the review job's group so neither cancels the other.
|
||||
concurrency:
|
||||
group: bm-bossbot-recheck-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Checkout trusted base ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Re-evaluate approval from review-thread state
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
uv run --script scripts/bm_bossbot_status.py recheck \
|
||||
--pr-number "${{ github.event.pull_request.number }}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
@@ -1,26 +1,18 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to review manually
|
||||
required: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Only run for organization members and collaborators
|
||||
if: |
|
||||
github.event.pull_request.author_association == 'OWNER' ||
|
||||
github.event.pull_request.author_association == 'MEMBER' ||
|
||||
github.event.pull_request.author_association == 'COLLABORATOR'
|
||||
|
||||
if: inputs.pr_number != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -43,7 +35,14 @@ jobs:
|
||||
track_progress: true # Enable visual progress tracking
|
||||
allowed_bots: '*'
|
||||
prompt: |
|
||||
Review this Basic Memory PR against our team checklist:
|
||||
Review Basic Memory PR #${{ inputs.pr_number }} as an advisory manual review.
|
||||
|
||||
Use `gh pr view ${{ inputs.pr_number }}` and related `gh pr`/`gh api`
|
||||
commands to inspect the pull request. Do not merge the PR and do not
|
||||
treat this advisory review as the required merge gate. BM Bossbot owns
|
||||
the required `BM Bossbot Approval` status.
|
||||
|
||||
Review the PR against our team checklist:
|
||||
|
||||
## Code Quality & Standards
|
||||
- [ ] Follows Basic Memory's coding conventions in CLAUDE.md
|
||||
|
||||
@@ -13,9 +13,10 @@ env:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@@ -24,10 +25,8 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Set up Depot
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
@@ -49,13 +48,12 @@ jobs:
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: ${{ vars.DEPOT_BASIC_MEMORY_PROJECT_ID || vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -38,6 +38,7 @@ jobs:
|
||||
mcp
|
||||
sync
|
||||
ui
|
||||
ci
|
||||
deps
|
||||
installer
|
||||
plugins
|
||||
|
||||
@@ -13,12 +13,15 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
# Required CI records pytest-testmon data but still runs the full selected suite.
|
||||
# The selective mode stays on explicit developer flows such as `just testmon`.
|
||||
BASIC_MEMORY_TESTMON_FLAGS: "--testmon-noselect"
|
||||
|
||||
jobs:
|
||||
static-checks:
|
||||
name: Static Checks (Python 3.12)
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -60,10 +63,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
- os: depot-ubuntu-24.04
|
||||
python-version: "3.12"
|
||||
- os: ubuntu-latest
|
||||
- os: depot-ubuntu-24.04
|
||||
python-version: "3.13"
|
||||
# Python 3.14 unit tests are the longest full-suite slice; keep this
|
||||
# one on GitHub-hosted runners after Depot terminated it mid-suite.
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.14"
|
||||
- os: windows-latest
|
||||
@@ -87,6 +92,19 @@ jobs:
|
||||
|
||||
- uses: extractions/setup-just@v4
|
||||
|
||||
- name: Cache pytest-testmon results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.testmondata
|
||||
.testmondata-shm
|
||||
.testmondata-wal
|
||||
key: ${{ runner.os }}-testmon-sqlite-unit-py${{ matrix.python-version }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-testmon-sqlite-unit-py${{ matrix.python-version }}-${{ github.ref_name }}-
|
||||
${{ runner.os }}-testmon-sqlite-unit-py${{ matrix.python-version }}-main-
|
||||
${{ runner.os }}-testmon-sqlite-unit-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -106,11 +124,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
- os: depot-ubuntu-24.04
|
||||
python-version: "3.12"
|
||||
- os: ubuntu-latest
|
||||
- os: depot-ubuntu-24.04
|
||||
python-version: "3.13"
|
||||
- os: ubuntu-latest
|
||||
- os: depot-ubuntu-24.04
|
||||
python-version: "3.14"
|
||||
- os: windows-latest
|
||||
python-version: "3.12"
|
||||
@@ -133,6 +151,19 @@ jobs:
|
||||
|
||||
- uses: extractions/setup-just@v4
|
||||
|
||||
- name: Cache pytest-testmon results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.testmondata
|
||||
.testmondata-shm
|
||||
.testmondata-wal
|
||||
key: ${{ runner.os }}-testmon-sqlite-integration-py${{ matrix.python-version }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-testmon-sqlite-integration-py${{ matrix.python-version }}-${{ github.ref_name }}-
|
||||
${{ runner.os }}-testmon-sqlite-integration-py${{ matrix.python-version }}-main-
|
||||
${{ runner.os }}-testmon-sqlite-integration-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -153,9 +184,14 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- python-version: "3.12"
|
||||
os: depot-ubuntu-24.04
|
||||
- python-version: "3.13"
|
||||
os: depot-ubuntu-24.04
|
||||
# Match the SQLite unit slice: this full Python 3.14 path outlived
|
||||
# the Depot runner and was terminated mid-suite.
|
||||
- python-version: "3.14"
|
||||
runs-on: ubuntu-latest
|
||||
os: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -190,6 +226,19 @@ jobs:
|
||||
|
||||
- uses: extractions/setup-just@v4
|
||||
|
||||
- name: Cache pytest-testmon results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.testmondata
|
||||
.testmondata-shm
|
||||
.testmondata-wal
|
||||
key: ${{ runner.os }}-testmon-postgres-unit-py${{ matrix.python-version }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-testmon-postgres-unit-py${{ matrix.python-version }}-${{ github.ref_name }}-
|
||||
${{ runner.os }}-testmon-postgres-unit-py${{ matrix.python-version }}-main-
|
||||
${{ runner.os }}-testmon-postgres-unit-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -212,7 +261,7 @@ jobs:
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
- python-version: "3.14"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -247,6 +296,19 @@ jobs:
|
||||
|
||||
- uses: extractions/setup-just@v4
|
||||
|
||||
- name: Cache pytest-testmon results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.testmondata
|
||||
.testmondata-shm
|
||||
.testmondata-wal
|
||||
key: ${{ runner.os }}-testmon-postgres-integration-py${{ matrix.python-version }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-testmon-postgres-integration-py${{ matrix.python-version }}-${{ github.ref_name }}-
|
||||
${{ runner.os }}-testmon-postgres-integration-py${{ matrix.python-version }}-main-
|
||||
${{ runner.os }}-testmon-postgres-integration-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
@@ -262,7 +324,7 @@ jobs:
|
||||
test-semantic:
|
||||
name: Test Semantic (Python 3.12)
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -281,6 +343,19 @@ jobs:
|
||||
|
||||
- uses: extractions/setup-just@v4
|
||||
|
||||
- name: Cache pytest-testmon results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.testmondata
|
||||
.testmondata-shm
|
||||
.testmondata-wal
|
||||
key: ${{ runner.os }}-testmon-semantic-py3.12-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-testmon-semantic-py3.12-${{ github.ref_name }}-
|
||||
${{ runner.os }}-testmon-semantic-py3.12-main-
|
||||
${{ runner.os }}-testmon-semantic-py3.12-
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
|
||||
@@ -49,12 +49,13 @@ ENV/
|
||||
/docs/.obsidian/
|
||||
/examples/.obsidian/
|
||||
/examples/.basic-memory/
|
||||
|
||||
/docs/assets
|
||||
|
||||
# claude action
|
||||
claude-output
|
||||
**/.claude/settings.local.json
|
||||
.mcp.json
|
||||
!/plugins/codex/.mcp.json
|
||||
.mcpregistry_*
|
||||
/.testmondata
|
||||
.benchmarks/
|
||||
|
||||
@@ -83,7 +83,7 @@ Before opening or updating a PR, run the checks that mirror the common required
|
||||
- Run `just typecheck` in addition to targeted `ruff` and `pytest` commands when tests were added or changed.
|
||||
- Sign commits with `git commit -s` so DCO passes. If a PR branch already has unsigned commits, rewrite the branch with signed-off commits before asking for review.
|
||||
- Use a semantic PR title accepted by `.github/workflows/pr-title.yml`: `type(scope): summary`.
|
||||
- Use one of the allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `deps`, `installer`, `plugins`, `skills`, `integrations`.
|
||||
- Use one of the allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `ci`, `deps`, `installer`, `plugins`, `skills`, `integrations`.
|
||||
|
||||
### Test Structure
|
||||
|
||||
@@ -108,13 +108,34 @@ Before opening or updating a PR, run the checks that mirror the common required
|
||||
- Follow the repository pattern for data access
|
||||
- Tools communicate to api routers via the httpx ASGI client (in process)
|
||||
|
||||
### Programming Style
|
||||
|
||||
See [docs/ENGINEERING_STYLE.md](docs/ENGINEERING_STYLE.md) for the fuller house style. The
|
||||
short version for agents:
|
||||
|
||||
- Prefer type-safe, explicit designs over object-heavy indirection. Use Python 3.12 `type`
|
||||
aliases, full annotations, and narrow `Protocol`s when a caller only needs a capability.
|
||||
- Use dataclasses for internal value objects and operation results; use Pydantic v2 at API,
|
||||
CLI, MCP, and persistence boundaries where validation and serialization matter.
|
||||
- Keep async boundaries obvious. Resource-owning code should use context managers, propagate
|
||||
cancellation, and avoid hidden background work unless the lifecycle is explicit.
|
||||
- Fail fast. Do not add silent fallback logic, broad exception swallowing, speculative
|
||||
`getattr`, or casts that hide an unclear model shape.
|
||||
- Keep control flow simple and local. Push branching decisions up, keep leaf helpers focused,
|
||||
and name values after the domain concept they carry.
|
||||
- Use evidence-first testing. Add or update meaningful regression tests for bugs and risky
|
||||
behavior, prefer real code paths over mocks, and run the narrowest command that proves the
|
||||
change before widening verification.
|
||||
- Comments should explain why a branch, invariant, or constraint exists. Avoid comments that
|
||||
merely narrate obvious code.
|
||||
|
||||
### Code Change Guidelines
|
||||
|
||||
- **Full file read before edits**: Before editing any file, read it in full first to ensure complete context; partial reads lead to corrupted edits
|
||||
- **Minimize diffs**: Prefer the smallest change that satisfies the request. Avoid unrelated refactors or style rewrites unless necessary for correctness
|
||||
- **No speculative getattr**: Never use `getattr(obj, "attr", default)` when unsure about attribute names. Check the class definition or source code first
|
||||
- **Fail fast**: Write code with fail-fast logic by default. Do not swallow exceptions with errors or warnings
|
||||
- **No fallback logic**: Do not add fallback logic unless explicitly told to and agreed with the user
|
||||
- **House style is canonical**: Follow the Programming Style section above for type-safe,
|
||||
fail-fast code; do not hide unclear models with speculative attributes, broad exception
|
||||
handling, casts, or unapproved fallback logic
|
||||
- **No guessing**: Do not say "The issue is..." before you actually know what the issue is. Investigate first.
|
||||
|
||||
### Literate Programming Style
|
||||
@@ -348,6 +369,13 @@ See `.claude/commands/release/release.md` (and `beta.md`, `release-check.md`, `c
|
||||
- Manage snapshots: `basic-memory cloud snapshot [create|list|delete|show|browse]`
|
||||
- Restore from snapshot: `basic-memory cloud restore <path> --snapshot <id>`
|
||||
|
||||
**Cloud Sync Commands (Personal and Team workspaces):**
|
||||
- Fetch cloud changes (cloud -> local): `basic-memory cloud pull --name "name"` (Team-safe; additive, never deletes local)
|
||||
- Upload local changes (local -> cloud): `basic-memory cloud push --name "name"` (Team-safe; additive, never deletes cloud)
|
||||
- Resolve conflicts on push/pull: `--on-conflict [fail|keep-local|keep-cloud|keep-both]` (default `fail` lists conflicts and aborts, git-style)
|
||||
- One-way mirror (local -> cloud): `basic-memory cloud sync --name "name"` (Personal workspaces only; deletes cloud files missing locally)
|
||||
- Two-way mirror (local <-> cloud): `basic-memory cloud bisync --name "name"` (Personal workspaces only)
|
||||
|
||||
### MCP Capabilities
|
||||
|
||||
- Basic Memory exposes these MCP tools to LLMs:
|
||||
@@ -506,7 +534,7 @@ With GitHub integration, the development workflow includes:
|
||||
5. **Code Commits**: ALWAYS sign off commits with `git commit -s`
|
||||
6. **Pull Request Titles**: PR titles must follow the semantic format enforced by `.github/workflows/pr-title.yml`: `type(scope): summary`
|
||||
- Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`
|
||||
- Allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `deps`, `installer`, `plugins`, `skills`, `integrations`
|
||||
- Allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `ci`, `deps`, `installer`, `plugins`, `skills`, `integrations`
|
||||
- Example: `fix(cli): propagate cloud workspace routing`
|
||||
|
||||
This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Unreleased
|
||||
## v0.21.6 (2026-06-04)
|
||||
|
||||
Monorepo consolidation plus a redesigned Claude Code plugin. The satellite
|
||||
repositories now live in the main `basic-memory` tree, and the plugin is
|
||||
rebuilt as a memory bridge with a guided setup interview, capture skills, and
|
||||
team workspaces. Codex, Hermes, and OpenClaw integration packages ship
|
||||
alongside it.
|
||||
|
||||
### Core
|
||||
|
||||
@@ -8,38 +14,59 @@
|
||||
`basic-memory` tree as the canonical source, discovery, documentation,
|
||||
issue, and release home.
|
||||
- Added root marketplace/package validation for the consolidated repository
|
||||
layout while keeping Phase 1 focused on parity.
|
||||
- Added root and package-local justfile targets so Claude Code, skills,
|
||||
layout.
|
||||
- Added root and package-local justfile targets so Claude Code, Codex, skills,
|
||||
Hermes, and OpenClaw builds can be verified from the monorepo.
|
||||
- Removed the legacy `ui/` directory.
|
||||
|
||||
### API
|
||||
|
||||
- Entity resolution now exposes the owning project on resolved entities so
|
||||
callers can route follow-up reads to the correct workspace/project.
|
||||
|
||||
### CLI
|
||||
|
||||
- Added an "auto BM" GitHub CI workflow that captures notes from CI runs.
|
||||
- Exposed project sync-support metadata and surfaced copyable workspace
|
||||
identifiers in project listings.
|
||||
- `cloud login` now surfaces non-subscription errors instead of masking them.
|
||||
- Team workspaces block `rclone sync`, with the team-workspace guard limited
|
||||
to `bisync`.
|
||||
|
||||
### Claude Code
|
||||
|
||||
- Rebuilt the Claude Code plugin as a memory bridge with SessionStart and
|
||||
PreCompact hooks, a bundled output style, and seeded note schemas.
|
||||
- Added the `/basic-memory:setup` bootstrap interview and the
|
||||
`/basic-memory:remember`, `/basic-memory:status`, and `/basic-memory:share`
|
||||
skills.
|
||||
- Added team workspace support with attribution for shared writes.
|
||||
- Prefixed plugin skills with `bm-` and installed the shared `memory-*` skills
|
||||
instead of duplicating them in the plugin.
|
||||
- Hooks fall back to `uvx`/`uv` when no CLI binary is on PATH.
|
||||
|
||||
### Codex
|
||||
|
||||
- Added the Codex plugin under `plugins/codex/` with its native
|
||||
`.codex-plugin`, hooks, seeded schemas, and `bm-*` skills.
|
||||
|
||||
### Skills
|
||||
|
||||
- Added the shared Basic Memory `SKILL.md` collection under top-level
|
||||
`skills/`.
|
||||
- Updated skills documentation to point at `basicmachines-co/basic-memory`
|
||||
and document manual copy as the temporary fallback when subpath installs are
|
||||
unavailable.
|
||||
|
||||
### Claude Code
|
||||
|
||||
- Added the Claude Code plugin under `plugins/claude-code/` with its native
|
||||
`.claude-plugin`, hooks, skills, agent, changelog, and docs.
|
||||
- Added the root Claude marketplace manifest pointing `basic-memory` to
|
||||
`./plugins/claude-code`.
|
||||
`skills/` and ported useful retired plugin skills into the `memory-*` set.
|
||||
- Fixed invalid picoschema enum YAML in the memory skills.
|
||||
|
||||
### Hermes
|
||||
|
||||
- Added the Hermes memory provider plugin under `integrations/hermes/` with
|
||||
its native Python module, `plugin.yaml`, skill, tests, docs, and release
|
||||
metadata.
|
||||
- Updated Hermes install and development docs for monorepo subpath usage.
|
||||
|
||||
### OpenClaw
|
||||
|
||||
- Added the OpenClaw plugin under `integrations/openclaw/` with its native
|
||||
npm/TypeScript package shape, tests, docs, and release metadata.
|
||||
- Updated OpenClaw package metadata to point at the monorepo subdirectory and
|
||||
changed skill bundling to copy from the top-level `skills/` source.
|
||||
npm/TypeScript package shape, tests, docs, and release metadata; skill
|
||||
bundling copies from the top-level `skills/` source.
|
||||
|
||||
## v0.21.5 (2026-05-26)
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ just package-check-openclaw
|
||||
|
||||
The Claude Code plugin is the bridge between Claude's working memory and Basic
|
||||
Memory — session-start briefings, pre-compaction checkpoints, an opt-in capture
|
||||
output style, and `/basic-memory:setup` · `:remember` · `:share` · `:status`.
|
||||
output style, and `/basic-memory:bm-setup` · `:remember` · `:share` · `:status`.
|
||||
|
||||
**Connect the Basic Memory MCP server first** — see [Connect your AI
|
||||
client](#connect-your-ai-client). The plugin's hooks and skills call it, so it's a
|
||||
@@ -216,14 +216,14 @@ Source: [`plugins/claude-code`](plugins/claude-code).
|
||||
### Shared skills
|
||||
|
||||
Framework-agnostic `SKILL.md` files live in [`skills/`](skills). If your
|
||||
Skills CLI supports subpath installs:
|
||||
Skills CLI supports repository subdirectory sources:
|
||||
|
||||
```bash
|
||||
npx skills add basicmachines-co/basic-memory --path skills
|
||||
npx skills add basicmachines-co/basic-memory/skills
|
||||
```
|
||||
|
||||
If it does not, copy the `memory-*` directories from `skills/` into your
|
||||
agent's skills directory as a temporary Phase 1 install path.
|
||||
If your installed Skills CLI cannot load that source, update the CLI or copy
|
||||
the `memory-*` directories from `skills/` into your agent's skills directory.
|
||||
|
||||
### Hermes
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Basic Memory Engineering Style
|
||||
|
||||
Style is how we make code easier to verify. Prefer explicit, typed, local-first code that
|
||||
preserves the file system as the source of truth while keeping the database, API, and MCP
|
||||
surfaces in sync.
|
||||
|
||||
## Design Center
|
||||
|
||||
- Basic Memory is local-first. Markdown files are the durable source; SQLite/Postgres indexes
|
||||
are derived state that should be rebuilt or reconciled from files when needed.
|
||||
- Keep the existing boundary order: CLI/MCP/API entrypoints compose dependencies, services own
|
||||
business behavior, repositories own database access, and file services own filesystem writes.
|
||||
- MCP tools should remain atomic and composable. They should call API routers through typed MCP
|
||||
clients, not reach around into services.
|
||||
- Prefer small, explicit abstractions that match a real domain boundary. Avoid object
|
||||
hierarchies when a function, dataclass, type alias, or protocol describes the concept better.
|
||||
|
||||
## Types And Data
|
||||
|
||||
- Use full type annotations and Python 3.12 syntax. Introduce `type` aliases for repeated
|
||||
structured shapes, callback signatures, or domain concepts that would otherwise become
|
||||
anonymous `dict[str, Any]` values.
|
||||
- Use dataclasses for internal values, operation inputs, and service results. Prefer
|
||||
`frozen=True` when the value should not change and `slots=True` when identity/dynamic
|
||||
attributes are not needed.
|
||||
- Use Pydantic v2 at boundaries that validate, serialize, or deserialize data: API payloads,
|
||||
CLI/MCP schemas, configuration, and persistence-adjacent schemas.
|
||||
- Use narrow `Protocol`s when a caller needs a capability rather than a concrete repository or
|
||||
service. Keep protocols small enough that fake implementations in tests are obvious.
|
||||
- Avoid speculative `getattr`, broad casts, or `Any` as a way to paper over uncertainty. Read
|
||||
the model or schema definition and make the type relationship explicit.
|
||||
|
||||
## Control Flow And Resources
|
||||
|
||||
- Fail fast when an invariant is broken. Do not swallow exceptions, add warning-only error
|
||||
handling, or introduce fallback behavior unless the user explicitly agrees to that behavior.
|
||||
- Keep control flow simple and close to the domain decision. Push `if` statements up into the
|
||||
function that owns orchestration; keep leaf helpers focused on computation or one side effect.
|
||||
- Make async/resource boundaries visible with context managers and explicit lifecycles. Do not
|
||||
start background work without a clear owner, cancellation story, and verification path.
|
||||
- Keep file mutations centralized through the existing file utilities/services so checksum,
|
||||
atomic write, and index synchronization behavior stays coherent.
|
||||
|
||||
## Testing And Verification
|
||||
|
||||
- Use evidence-first testing, not mechanical TDD. For bugs and risky behavior, add or update a
|
||||
regression test that would catch the failure. For small documentation-only edits, use the
|
||||
relevant doc/repo hygiene checks.
|
||||
- Prefer tests that exercise real code paths. Use mocks, doubles, or `monkeypatch` only when
|
||||
the external boundary would be slow, nondeterministic, or impossible to trigger directly.
|
||||
- Keep coverage at 100% for new code. Use `# pragma: no cover` only for code that would require
|
||||
disproportionate mocking and is covered through an integration or runtime path.
|
||||
- Start with targeted commands, then widen as risk grows: focused pytest, `just fast-check`,
|
||||
`just doctor`, package checks for agent packaging changes, and full SQLite/Postgres gates
|
||||
when behavior crosses shared boundaries.
|
||||
|
||||
## Comments And Names
|
||||
|
||||
- Name values after the domain concept they carry: project, entity, permalink, tenant, route,
|
||||
checksum, observation, relation, batch, or index state.
|
||||
- Comments should say why a branch, invariant, retry, lifecycle, or compatibility constraint
|
||||
exists. Section headers are useful when a function or file has clear phases.
|
||||
- Avoid comments that restate the code. If a comment cannot explain a decision, simplify the
|
||||
code or improve the name instead.
|
||||
@@ -8,9 +8,25 @@ The cloud CLI enables you to:
|
||||
- **Authenticate cloud access** - OAuth/API key credentials are stored locally for cloud operations
|
||||
- **Project-scoped sync** - Each project independently manages its sync configuration
|
||||
- **Explicit operations** - Sync only what you want, when you want
|
||||
- **Bidirectional sync** - Keep local and cloud in sync with rclone bisync
|
||||
- **Team-safe push/pull** - Additive, git-style transfers that work on shared Team workspaces
|
||||
- **Bidirectional sync** - Keep local and cloud in sync with rclone bisync (Personal workspaces)
|
||||
- **Offline access** - Work locally, sync when ready
|
||||
|
||||
### Personal vs Team workspaces
|
||||
|
||||
The transfer commands fall into two groups:
|
||||
|
||||
| Command | Direction | Behavior | Personal | Team |
|
||||
|---|---|---|---|---|
|
||||
| `bm cloud pull` | cloud → local | **additive** — never deletes local | ✅ | ✅ |
|
||||
| `bm cloud push` | local → cloud | **additive** — never deletes cloud | ✅ | ✅ |
|
||||
| `bm cloud sync` | local → cloud | **mirror** — deletes cloud files missing locally | ✅ | ❌ |
|
||||
| `bm cloud bisync` | local ↔ cloud | **mirror** — two-way, deletes on both sides | ✅ | ❌ |
|
||||
|
||||
`sync` and `bisync` are mirror operations: one local tree becomes authoritative and files missing on the other side get deleted. That is correct for a Personal workspace (one user, one source of truth) but unsafe on a shared Team bucket, where it could delete a teammate's files. On Team workspaces these commands exit early with a clear error and point you at `push`/`pull`.
|
||||
|
||||
`push` and `pull` are additive (they use `rclone copy`, which never deletes on the destination), so they are safe on both Personal and Team workspaces.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using Basic Memory Cloud, you need:
|
||||
@@ -55,8 +71,8 @@ bm project add work --cloud --local-path ~/work-notes
|
||||
bm project add temp --cloud # No local sync
|
||||
|
||||
# Now you can sync individually (after initial --resync):
|
||||
bm project bisync --name research
|
||||
bm project bisync --name work
|
||||
bm cloud bisync --name research
|
||||
bm cloud bisync --name work
|
||||
# temp stays cloud-only
|
||||
```
|
||||
|
||||
@@ -137,10 +153,10 @@ Establish the initial sync baseline. **Best practice:** Always preview with `--d
|
||||
|
||||
```bash
|
||||
# Step 1: Preview the initial sync (recommended)
|
||||
bm project bisync --name research --resync --dry-run
|
||||
bm cloud bisync --name research --resync --dry-run
|
||||
|
||||
# Step 2: If all looks good, run the actual sync
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
**What happens under the covers:**
|
||||
@@ -167,7 +183,7 @@ This will effectively make both Path1 and Path2 filesystems contain a matching s
|
||||
After the first sync, just run bisync without `--resync`:
|
||||
|
||||
```bash
|
||||
bm project bisync --name research
|
||||
bm cloud bisync --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
@@ -235,7 +251,7 @@ bm project add research --cloud --local-path ~/Documents/research
|
||||
- Stores sync config in `~/.basic-memory/config.json`
|
||||
- Prepares for bisync (but doesn't sync yet)
|
||||
|
||||
**Result:** Project ready to sync. Run `bm project bisync --name research --resync` to establish baseline.
|
||||
**Result:** Project ready to sync. Run `bm cloud bisync --name research --resync` to establish baseline.
|
||||
|
||||
**Use case 3: Add sync to existing cloud project**
|
||||
|
||||
@@ -294,18 +310,98 @@ For MCP stdio, routing is always local.
|
||||
|
||||
### Understanding the Sync Commands
|
||||
|
||||
**There are three sync-related commands:**
|
||||
**There are five sync-related commands:**
|
||||
|
||||
1. `bm project sync` - One-way: local → cloud (make cloud match local)
|
||||
2. `bm project bisync` - Two-way: local ↔ cloud (recommended)
|
||||
3. `bm project check` - Verify files match (no changes)
|
||||
| Command | Direction | Workspace | Summary |
|
||||
|---|---|---|---|
|
||||
| `bm cloud pull` | cloud → local | Personal + Team | Fetch cloud changes, additively (git-style) |
|
||||
| `bm cloud push` | local → cloud | Personal + Team | Upload local changes, additively (git-style) |
|
||||
| `bm cloud sync` | local → cloud | Personal only | One-way mirror (cloud becomes identical to local) |
|
||||
| `bm cloud bisync` | local ↔ cloud | Personal only | Two-way mirror (recommended for solo use) |
|
||||
| `bm cloud check` | — | Personal + Team | Verify files match (no changes) |
|
||||
|
||||
### One-Way Sync: Local → Cloud
|
||||
If you collaborate on a shared Team workspace, use **`push`/`pull`** (see [Team Workspaces](#team-workspaces-push--pull-additive-git-style)). If you are the only writer (a Personal workspace), the mirror commands `sync`/`bisync` give you a single source of truth.
|
||||
|
||||
### Team Workspaces: push / pull (additive, git-style)
|
||||
|
||||
`push` and `pull` are the Team-safe transfer commands. They model `git push` / `git pull`:
|
||||
|
||||
- **`bm cloud pull`** fetches changes from the cloud into your local directory.
|
||||
- **`bm cloud push`** uploads your local changes to the cloud.
|
||||
|
||||
Both use `rclone copy`, so they are **additive — they never delete on the destination**. A conflict (a file that differs on both sides) is never resolved silently: by default the command aborts and lists the conflicting files, exactly like git refusing to clobber your changes.
|
||||
|
||||
#### Pull: fetch cloud changes
|
||||
|
||||
```bash
|
||||
# Preview first (recommended)
|
||||
bm cloud pull --name research --dry-run
|
||||
|
||||
# Fetch new/changed cloud files into local
|
||||
bm cloud pull --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Compares cloud and local with `rclone check`
|
||||
2. Downloads files that are new or changed on the cloud
|
||||
3. Leaves your local-only files untouched (never deletes local)
|
||||
4. If any file differs on both sides, aborts and lists the conflicts (unless you pass `--on-conflict`)
|
||||
|
||||
#### Push: upload local changes
|
||||
|
||||
```bash
|
||||
bm cloud push --name research --dry-run
|
||||
bm cloud push --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Compares local and cloud with `rclone check`
|
||||
2. Uploads files that are new or changed locally
|
||||
3. Leaves cloud-only files untouched (never deletes cloud)
|
||||
4. If any file differs on both sides, aborts and lists the conflicts — pull first, like a rejected `git push`
|
||||
|
||||
#### Resolving conflicts
|
||||
|
||||
When `push`/`pull` reports conflicts, re-run with `--on-conflict` to choose how differing files are handled. The value names exactly what survives, so it reads the same in both directions:
|
||||
|
||||
| `--on-conflict` | Behavior |
|
||||
|---|---|
|
||||
| `fail` *(default)* | List the conflicting files and exit without transferring anything |
|
||||
| `keep-cloud` | Take the cloud version (pull: overwrite local; push: skip those files) |
|
||||
| `keep-local` | Keep the local version (pull: skip those files; push: overwrite cloud) |
|
||||
| `keep-both` | Keep both — write the incoming version beside the existing one as `name.conflict-<date>.md` |
|
||||
|
||||
```bash
|
||||
# A teammate edited notes you also changed locally — pull reports a conflict:
|
||||
bm cloud pull --name research
|
||||
# pull aborted: 1 file(s) differ between local and cloud.
|
||||
# * notes/decisions.md
|
||||
# Re-run with one of: --on-conflict keep-cloud | keep-local | keep-both
|
||||
|
||||
# Take the cloud copy:
|
||||
bm cloud pull --name research --on-conflict keep-cloud
|
||||
|
||||
# Or keep both versions to merge by hand:
|
||||
bm cloud pull --name research --on-conflict keep-both
|
||||
```
|
||||
|
||||
#### Limitations
|
||||
|
||||
`push`/`pull` are deliberately simple, conflict-aware byte transfers — not a full reconciler. Without a sync baseline:
|
||||
|
||||
- **Deletions are not propagated.** A note deleted on one side is not removed from the other (we cannot tell an intentional delete from a file the other side never had). This is surfaced in the command output.
|
||||
- **Every divergence is treated as a conflict.** We cannot tell a teammate's edit from your stale copy, so any differing file prompts a decision rather than auto-resolving.
|
||||
|
||||
For conflict-aware *editing*, write through the MCP/API tools (which merge at the note level). A Team-safe bidirectional reconciler with a real baseline is tracked in [issue #862](https://github.com/basicmachines-co/basic-memory/issues/862).
|
||||
|
||||
### One-Way Sync: Local → Cloud (Personal only)
|
||||
|
||||
**Use case:** You made changes locally and want to push to cloud (overwrite cloud).
|
||||
|
||||
> **Personal workspaces only.** `sync` is a destructive mirror — it deletes cloud files that are not present locally. On a Team workspace it would delete a teammate's files, so it is blocked there. Use `bm cloud push` (additive) on Team workspaces.
|
||||
|
||||
```bash
|
||||
bm project sync --name research
|
||||
bm cloud sync --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
@@ -321,16 +417,18 @@ bm project sync --name research
|
||||
- You want to force cloud to match local
|
||||
- You don't care about cloud changes
|
||||
|
||||
### Two-Way Sync: Local ↔ Cloud (Recommended)
|
||||
### Two-Way Sync: Local ↔ Cloud (Personal only, recommended for solo use)
|
||||
|
||||
**Use case:** You edit files both locally and in cloud UI, want both to stay in sync.
|
||||
|
||||
> **Personal workspaces only.** `bisync` is a two-way mirror that can delete and overwrite on both sides. It is blocked on Team workspaces — use `bm cloud pull` then `bm cloud push` there. A Team-safe bidirectional reconciler is tracked separately ([issue #862](https://github.com/basicmachines-co/basic-memory/issues/862)).
|
||||
|
||||
```bash
|
||||
# First time - establish baseline
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
|
||||
# Subsequent syncs
|
||||
bm project bisync --name research
|
||||
bm cloud bisync --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
@@ -349,7 +447,7 @@ echo "Local change" > ~/Documents/research/notes.md
|
||||
# Cloud now has: "Cloud change"
|
||||
|
||||
# Run bisync
|
||||
bm project bisync --name research
|
||||
bm cloud bisync --name research
|
||||
|
||||
# Result: Newer file wins (based on modification time)
|
||||
# If cloud was more recent, cloud version kept
|
||||
@@ -366,7 +464,7 @@ bm project bisync --name research
|
||||
**Use case:** Check if local and cloud match without making changes.
|
||||
|
||||
```bash
|
||||
bm project check --name research
|
||||
bm cloud check --name research
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
@@ -378,7 +476,7 @@ bm project check --name research
|
||||
|
||||
```bash
|
||||
# One-way check (faster)
|
||||
bm project check --name research --one-way
|
||||
bm cloud check --name research --one-way
|
||||
```
|
||||
|
||||
### Preview Changes (Dry Run)
|
||||
@@ -386,7 +484,7 @@ bm project check --name research --one-way
|
||||
**Use case:** See what would change without actually syncing.
|
||||
|
||||
```bash
|
||||
bm project bisync --name research --dry-run
|
||||
bm cloud bisync --name research --dry-run
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
@@ -432,20 +530,20 @@ bm project add work --cloud --local-path ~/work-notes
|
||||
bm project add personal --cloud --local-path ~/personal
|
||||
|
||||
# Establish baselines
|
||||
bm project bisync --name research --resync
|
||||
bm project bisync --name work --resync
|
||||
bm project bisync --name personal --resync
|
||||
bm cloud bisync --name research --resync
|
||||
bm cloud bisync --name work --resync
|
||||
bm cloud bisync --name personal --resync
|
||||
|
||||
# Daily workflow: sync everything
|
||||
bm project bisync --name research
|
||||
bm project bisync --name work
|
||||
bm project bisync --name personal
|
||||
bm cloud bisync --name research
|
||||
bm cloud bisync --name work
|
||||
bm cloud bisync --name personal
|
||||
```
|
||||
|
||||
**Future:** `--all` flag will sync all configured projects:
|
||||
|
||||
```bash
|
||||
bm project bisync --all # Coming soon
|
||||
bm cloud bisync --all # Coming soon
|
||||
```
|
||||
|
||||
### Mixed Usage
|
||||
@@ -462,8 +560,8 @@ bm project add archive --cloud
|
||||
bm project add temp-notes --cloud
|
||||
|
||||
# Sync only the configured ones
|
||||
bm project bisync --name research
|
||||
bm project bisync --name work
|
||||
bm cloud bisync --name research
|
||||
bm cloud bisync --name work
|
||||
|
||||
# Archive and temp-notes stay cloud-only
|
||||
```
|
||||
@@ -661,7 +759,7 @@ code ~/.basic-memory/.bmignore
|
||||
echo "*.tmp" >> ~/.basic-memory/.bmignore
|
||||
|
||||
# Next sync uses updated patterns
|
||||
bm project bisync --name research
|
||||
bm cloud bisync --name research
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -724,7 +822,7 @@ bm cloud login
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
@@ -747,7 +845,7 @@ bm project bisync --name research --resync
|
||||
echo "# Research Notes" > ~/Documents/research/README.md
|
||||
|
||||
# Now run bisync
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
**Why this happens:** Bisync creates listing files that track the state of each side. When both directories are completely empty, these listing files are considered invalid by rclone.
|
||||
@@ -764,10 +862,10 @@ bm project bisync --name research --resync
|
||||
|
||||
```bash
|
||||
# Clear bisync state
|
||||
bm project bisync-reset research
|
||||
bm cloud bisync-reset research
|
||||
|
||||
# Re-establish baseline
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
@@ -787,16 +885,16 @@ bm project bisync --name research --resync
|
||||
|
||||
```bash
|
||||
# Check what would be deleted
|
||||
bm project bisync --name research --dry-run
|
||||
bm cloud bisync --name research --dry-run
|
||||
|
||||
# If correct, establish new baseline
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
**Solution 2:** Use one-way sync if you know local is correct:
|
||||
|
||||
```bash
|
||||
bm project sync --name research
|
||||
bm cloud sync --name research
|
||||
```
|
||||
|
||||
### Project Not Configured for Sync
|
||||
@@ -809,7 +907,7 @@ bm project sync --name research
|
||||
|
||||
```bash
|
||||
bm cloud sync-setup research ~/Documents/research
|
||||
bm project bisync --name research --resync
|
||||
bm cloud bisync --name research --resync
|
||||
```
|
||||
|
||||
### Connection Issues
|
||||
@@ -880,20 +978,30 @@ bm project set-local <name> # Revert project to local mode
|
||||
### File Synchronization
|
||||
|
||||
```bash
|
||||
# One-way sync (local → cloud)
|
||||
bm project sync --name <project>
|
||||
bm project sync --name <project> --dry-run
|
||||
bm project sync --name <project> --verbose
|
||||
# Pull: fetch cloud changes (cloud → local) - Personal + Team, additive
|
||||
bm cloud pull --name <project>
|
||||
bm cloud pull --name <project> --dry-run
|
||||
bm cloud pull --name <project> --on-conflict [fail|keep-local|keep-cloud|keep-both]
|
||||
|
||||
# Two-way sync (local ↔ cloud) - Recommended
|
||||
bm project bisync --name <project> # After first --resync
|
||||
bm project bisync --name <project> --resync # First time / force baseline
|
||||
bm project bisync --name <project> --dry-run
|
||||
bm project bisync --name <project> --verbose
|
||||
# Push: upload local changes (local → cloud) - Personal + Team, additive
|
||||
bm cloud push --name <project>
|
||||
bm cloud push --name <project> --dry-run
|
||||
bm cloud push --name <project> --on-conflict [fail|keep-local|keep-cloud|keep-both]
|
||||
|
||||
# One-way mirror (local → cloud) - Personal workspaces only
|
||||
bm cloud sync --name <project>
|
||||
bm cloud sync --name <project> --dry-run
|
||||
bm cloud sync --name <project> --verbose
|
||||
|
||||
# Two-way mirror (local ↔ cloud) - Personal workspaces only
|
||||
bm cloud bisync --name <project> # After first --resync
|
||||
bm cloud bisync --name <project> --resync # First time / force baseline
|
||||
bm cloud bisync --name <project> --dry-run
|
||||
bm cloud bisync --name <project> --verbose
|
||||
|
||||
# Integrity check
|
||||
bm project check --name <project>
|
||||
bm project check --name <project> --one-way
|
||||
bm cloud check --name <project>
|
||||
bm cloud check --name <project> --one-way
|
||||
|
||||
# List project files by route
|
||||
bm project ls --name <project> # Default target: local
|
||||
@@ -909,15 +1017,25 @@ bm project ls --name <project> --cloud --path <subpath>
|
||||
1. **Authenticate cloud access** - `bm cloud login`
|
||||
2. **Install rclone** - `bm cloud setup`
|
||||
3. **Add projects with sync** - `bm project add research --cloud --local-path ~/Documents/research`
|
||||
4. **Preview first sync** - `bm project bisync --name research --resync --dry-run`
|
||||
5. **Establish baseline** - `bm project bisync --name research --resync`
|
||||
6. **Daily workflow** - `bm project bisync --name research`
|
||||
|
||||
**Personal workspace (solo, mirror) workflow:**
|
||||
|
||||
4. **Preview first sync** - `bm cloud bisync --name research --resync --dry-run`
|
||||
5. **Establish baseline** - `bm cloud bisync --name research --resync`
|
||||
6. **Daily workflow** - `bm cloud bisync --name research`
|
||||
|
||||
**Team workspace (shared, additive) workflow:**
|
||||
|
||||
4. **Fetch teammates' changes** - `bm cloud pull --name research`
|
||||
5. **Upload your changes** - `bm cloud push --name research`
|
||||
6. **Resolve conflicts explicitly** - re-run with `--on-conflict keep-cloud|keep-local|keep-both`
|
||||
|
||||
**Key benefits:**
|
||||
- ✅ Each project independently syncs (or doesn't)
|
||||
- ✅ Projects can live anywhere on disk
|
||||
- ✅ Explicit sync operations (no magic)
|
||||
- ✅ Safe by design (max delete limits, conflict resolution)
|
||||
- ✅ Team-safe push/pull that never delete on the destination
|
||||
- ✅ Safe by design (max delete limits, conflict resolution, git-style conflict aborts)
|
||||
- ✅ Full offline access (work locally, sync when ready)
|
||||
|
||||
**Future enhancements:**
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# LiteLLM Provider
|
||||
|
||||
Basic Memory can use the LiteLLM SDK for semantic search embeddings. This lets you
|
||||
keep Basic Memory's vector indexing and search behavior while routing embedding calls
|
||||
to OpenAI-compatible and provider-specific backends such as OpenAI, Azure OpenAI,
|
||||
Cohere, Bedrock, NVIDIA NIM, and other LiteLLM-supported embedding providers.
|
||||
|
||||
Use this page when you want to try a non-default embedding model, validate a provider,
|
||||
or tune LiteLLM-specific settings.
|
||||
|
||||
> **Experimental — advanced users only.** The LiteLLM provider is experimental and
|
||||
> intended for users who are comfortable operating remote embedding backends. It makes
|
||||
> paid, networked API calls, requires per-model dimension and input-role configuration,
|
||||
> and reindexing a real corpus can be slow and spend provider quota (see
|
||||
> [Reindexing with a remote provider](#reindexing-with-a-remote-provider)). For most
|
||||
> users, the default local **FastEmbed** provider is the recommended choice. Use LiteLLM
|
||||
> only if you know what you're doing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The default LiteLLM model is OpenAI `text-embedding-3-small` through the LiteLLM
|
||||
model string `openai/text-embedding-3-small`.
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
bm reindex --embeddings
|
||||
```
|
||||
|
||||
Then use vector or hybrid search:
|
||||
|
||||
```python
|
||||
search_notes("login token flow", search_type="hybrid")
|
||||
```
|
||||
|
||||
## Basic Memory Options
|
||||
|
||||
All options can be set in config or as environment variables.
|
||||
|
||||
| Config Field | Env Var | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `semantic_search_enabled` | `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED` | Auto | Set to `true` to force vector/hybrid support on. |
|
||||
| `semantic_embedding_provider` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER` | `fastembed` | Set to `litellm` for the LiteLLM provider. |
|
||||
| `semantic_embedding_model` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL` | `bge-small-en-v1.5` | With `litellm`, the default is remapped to `openai/text-embedding-3-small`. |
|
||||
| `semantic_embedding_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS` | Provider default | Required for non-default LiteLLM models because vector tables are dimensioned before the first API call. |
|
||||
| `semantic_embedding_forward_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_FORWARD_DIMENSIONS` | Auto | Sends `dimensions` to LiteLLM only when supported. Auto is enabled for `text-embedding-3` model strings. |
|
||||
| `semantic_embedding_document_input_type` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DOCUMENT_INPUT_TYPE` | Auto | LiteLLM `input_type` for indexed notes/passages. |
|
||||
| `semantic_embedding_query_input_type` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_QUERY_INPUT_TYPE` | Auto | LiteLLM `input_type` for search queries. |
|
||||
| `semantic_embedding_batch_size` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE` | `2` | Number of text chunks per provider request. |
|
||||
| `semantic_embedding_request_concurrency` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_REQUEST_CONCURRENCY` | `4` | Maximum concurrent LiteLLM embedding requests. |
|
||||
| `semantic_embedding_sync_batch_size` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_SYNC_BATCH_SIZE` | `2` | Number of prepared vector jobs flushed through the sync pipeline together. |
|
||||
|
||||
## Dimensions
|
||||
|
||||
Basic Memory needs the vector dimension before it can create SQLite or Postgres
|
||||
vector tables. The OpenAI default is known, so this works without an explicit
|
||||
dimension:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=openai/text-embedding-3-small
|
||||
```
|
||||
|
||||
For every other LiteLLM model, set the dimension explicitly:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=cohere/embed-english-v3.0
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=1024
|
||||
```
|
||||
|
||||
For fixed-size models, `semantic_embedding_dimensions` is Basic Memory's local
|
||||
schema and validation size. For OpenAI/Azure `text-embedding-3` models, LiteLLM
|
||||
can also forward `dimensions` as a provider-side reduced-output request. Basic
|
||||
Memory enables that automatically when the model string contains `text-embedding-3`.
|
||||
|
||||
If you use an Azure deployment alias such as `azure/<deployment-name>`, the model
|
||||
string may not reveal that the underlying model supports reduced output dimensions.
|
||||
Set this only when your deployment supports it:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_FORWARD_DIMENSIONS=true
|
||||
```
|
||||
|
||||
## Asymmetric Models
|
||||
|
||||
Some embedding models use different request roles for indexed documents and
|
||||
search queries. Basic Memory automatically sets these for known LiteLLM families:
|
||||
|
||||
| Model Family | Document `input_type` | Query `input_type` |
|
||||
|---|---|---|
|
||||
| Cohere v3 embeddings | `search_document` | `search_query` |
|
||||
| NVIDIA NIM retrieval embeddings | `passage` | `query` |
|
||||
|
||||
For any other asymmetric model, configure both roles explicitly:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DOCUMENT_INPUT_TYPE=passage
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_QUERY_INPUT_TYPE=query
|
||||
```
|
||||
|
||||
Changing provider, model, dimensions, dimension-forwarding, or document/query
|
||||
roles changes the meaning of stored vectors. Rebuild embeddings after any of
|
||||
those changes:
|
||||
|
||||
```bash
|
||||
bm reindex --embeddings
|
||||
```
|
||||
|
||||
## Reindexing with a remote provider
|
||||
|
||||
Embedding a real corpus through a network API is far slower than local FastEmbed, and
|
||||
the defaults are tuned for the local case. Two things to know before you run a full
|
||||
reindex.
|
||||
|
||||
**Raise the sync batch size.** `semantic_embedding_sync_batch_size` defaults to `2`, and
|
||||
it — not `semantic_embedding_batch_size` — governs throughput on the sync pipeline. With
|
||||
the default, a full reindex can take tens of seconds *per note* against a remote provider.
|
||||
Raising both to a larger value turns a multi-minute (or longer) reindex into well under a
|
||||
minute for the same corpus:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_SYNC_BATCH_SIZE=32
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE=64
|
||||
```
|
||||
|
||||
Stay within the provider's per-request size and rate limits — Cohere v3, for example,
|
||||
accepts up to 96 inputs per embedding request.
|
||||
|
||||
**Changing dimensions requires recreating the vector table.** Basic Memory dimensions the
|
||||
vector table on first index and refuses to mix sizes. Switching to a model with a
|
||||
different dimension (for example FastEmbed 384 → OpenAI 1536 → Cohere 1024) makes a plain
|
||||
`bm reindex` raise an `Embedding dimension mismatch` error. Recreate the table with a full
|
||||
rebuild — files are the source of truth, so this re-indexes from disk and re-embeds
|
||||
everything:
|
||||
|
||||
```bash
|
||||
bm reset --reindex
|
||||
```
|
||||
|
||||
To trial a provider without disturbing your existing index, point Basic Memory at a
|
||||
throwaway config + database instead:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_CONFIG_DIR=/tmp/bm-litellm-trial
|
||||
```
|
||||
|
||||
## Provider Setup Examples
|
||||
|
||||
LiteLLM reads provider credentials from the environment. These are the examples
|
||||
covered by Basic Memory's live validation harness.
|
||||
|
||||
### OpenAI Through LiteLLM
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=openai/text-embedding-3-small
|
||||
```
|
||||
|
||||
### Cohere v3
|
||||
|
||||
```bash
|
||||
export COHERE_API_KEY=...
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=cohere/embed-english-v3.0
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=1024
|
||||
```
|
||||
|
||||
The provider auto-selects `search_document` for indexed chunks and `search_query`
|
||||
for search queries.
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
```bash
|
||||
export AZURE_API_KEY=...
|
||||
export AZURE_API_BASE=https://<resource-name>.openai.azure.com
|
||||
export AZURE_API_VERSION=2024-02-01
|
||||
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=azure/<deployment-name>
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=1536
|
||||
```
|
||||
|
||||
If your Azure deployment is a reduced-dimension `text-embedding-3` deployment,
|
||||
set the dimension you want and enable forwarding:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=512
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_FORWARD_DIMENSIONS=true
|
||||
```
|
||||
|
||||
### NVIDIA NIM
|
||||
|
||||
```bash
|
||||
export NVIDIA_NIM_API_KEY=...
|
||||
# Optional when using a custom or self-hosted NIM endpoint:
|
||||
export NVIDIA_NIM_API_BASE=https://integrate.api.nvidia.com/v1
|
||||
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=nvidia_nim/nvidia/embed-qa-4
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=1024
|
||||
```
|
||||
|
||||
The provider auto-selects `passage` for indexed chunks and `query` for search
|
||||
queries.
|
||||
|
||||
## Testing LiteLLM Providers
|
||||
|
||||
Run the non-live LiteLLM unit and harness tests first:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/repository/test_litellm_provider.py \
|
||||
test-int/semantic/test_litellm_live_harness.py -q
|
||||
```
|
||||
|
||||
Run the SQLite and Postgres vector identity regressions when changing model
|
||||
identity, role, or vector sync behavior:
|
||||
|
||||
```bash
|
||||
uv run pytest \
|
||||
tests/repository/test_sqlite_vector_search_repository.py::test_sqlite_embedding_model_key_includes_litellm_role_settings \
|
||||
-q
|
||||
|
||||
BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest \
|
||||
tests/repository/test_postgres_search_repository.py::test_postgres_litellm_role_change_reembeds_existing_chunks \
|
||||
-q
|
||||
```
|
||||
|
||||
The Postgres command uses testcontainers, so Docker must be running.
|
||||
|
||||
## Live Provider Harness
|
||||
|
||||
The live harness makes real LiteLLM API calls and spends provider quota. It is
|
||||
opt-in by design:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export COHERE_API_KEY=...
|
||||
|
||||
just test-litellm-live
|
||||
```
|
||||
|
||||
Built-in cases run when their API keys are present:
|
||||
|
||||
| Case | Required Env Var | Validates |
|
||||
|---|---|---|
|
||||
| `openai-text-embedding-3-small` | `OPENAI_API_KEY` | OpenAI via LiteLLM, 1536 dimensions, normalized vectors, ranking sanity. |
|
||||
| `cohere-embed-english-v3` | `COHERE_API_KEY` | Cohere v3 role handling, 1024 dimensions, normalized vectors, ranking sanity. |
|
||||
|
||||
Add provider aliases or new backends with a custom cases file:
|
||||
|
||||
```bash
|
||||
cat > /tmp/litellm-cases.json <<'JSON'
|
||||
[
|
||||
{
|
||||
"name": "azure-text-embedding-3-small-512",
|
||||
"model": "azure/<deployment-name>",
|
||||
"dimensions": 512,
|
||||
"api_key_env": "AZURE_API_KEY",
|
||||
"forward_dimensions": true
|
||||
},
|
||||
{
|
||||
"name": "nvidia-embed-qa-4",
|
||||
"model": "nvidia_nim/nvidia/embed-qa-4",
|
||||
"dimensions": 1024,
|
||||
"api_key_env": "NVIDIA_NIM_API_KEY",
|
||||
"document_input_type": "passage",
|
||||
"query_input_type": "query"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
just test-litellm-live --cases-file /tmp/litellm-cases.json
|
||||
```
|
||||
|
||||
For CI-style output:
|
||||
|
||||
```bash
|
||||
just test-litellm-live --cases-file /tmp/litellm-cases.json --json
|
||||
```
|
||||
|
||||
The harness embeds two documents and one query, validates dimension and vector
|
||||
normalization, checks that the authentication query ranks the authentication
|
||||
document above a distractor, and reports latency plus role/dimension settings.
|
||||
|
||||
## Provider Reference
|
||||
|
||||
LiteLLM's own provider and embedding docs are the source of truth for current
|
||||
model strings and credential names:
|
||||
|
||||
- [LiteLLM embedding models](https://docs.litellm.ai/docs/embedding/supported_embedding)
|
||||
- [LiteLLM Azure OpenAI provider](https://docs.litellm.ai/docs/providers/azure)
|
||||
- [LiteLLM NVIDIA NIM provider](https://docs.litellm.ai/docs/providers/nvidia_nim)
|
||||
@@ -99,10 +99,13 @@ All settings are fields on `BasicMemoryConfig` and can be set via environment va
|
||||
| Config Field | Env Var | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `semantic_search_enabled` | `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED` | Auto (`true` when semantic deps are available) | Enable semantic search. Required before vector/hybrid modes work. |
|
||||
| `semantic_embedding_provider` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER` | `"fastembed"` | Embedding provider: `"fastembed"` (local) or `"openai"` (API). |
|
||||
| `semantic_embedding_provider` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER` | `"fastembed"` | Embedding provider: `"fastembed"` (local), `"openai"` (API), or `"litellm"` (multi-provider API, **experimental** — advanced users only). |
|
||||
| `semantic_embedding_model` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL` | `"bge-small-en-v1.5"` | Model identifier. Auto-adjusted per provider if left at default. |
|
||||
| `semantic_embedding_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS` | Auto-detected | Vector dimensions. 384 for FastEmbed, 1536 for OpenAI. Override only if using a non-default model. |
|
||||
| `semantic_embedding_batch_size` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE` | `64` | Number of texts to embed per batch. |
|
||||
| `semantic_embedding_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS` | Provider default | Vector dimensions. 384 for FastEmbed, 1536 for OpenAI/LiteLLM OpenAI. Required when using a non-default LiteLLM model. |
|
||||
| `semantic_embedding_forward_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_FORWARD_DIMENSIONS` | Auto | LiteLLM-only override for whether configured dimensions are sent as a provider-side output-size request. |
|
||||
| `semantic_embedding_batch_size` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE` | `2` | Number of texts to embed per batch. |
|
||||
| `semantic_embedding_document_input_type` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DOCUMENT_INPUT_TYPE` | Auto for known LiteLLM models | Optional LiteLLM `input_type` for indexed document/passages. |
|
||||
| `semantic_embedding_query_input_type` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_QUERY_INPUT_TYPE` | Auto for known LiteLLM models | Optional LiteLLM `input_type` for search queries. |
|
||||
| `semantic_vector_k` | `BASIC_MEMORY_SEMANTIC_VECTOR_K` | `100` | Candidate count for vector nearest-neighbour retrieval. Higher values improve recall at the cost of latency. |
|
||||
|
||||
## Embedding Providers
|
||||
@@ -135,7 +138,114 @@ export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=openai
|
||||
export OPENAI_API_KEY=sk-...
|
||||
```
|
||||
|
||||
When switching from FastEmbed to OpenAI (or vice versa), you must rebuild embeddings since the vector dimensions differ:
|
||||
### LiteLLM
|
||||
|
||||
> **Experimental — advanced users only.** The LiteLLM provider is experimental and aimed at users comfortable operating remote embedding backends: paid API calls, per-model dimension and input-role configuration, and slower reindexing of large corpora. For most users, FastEmbed (local, default) is recommended. See [LiteLLM Provider](litellm-provider.md) for the caveats and tuning.
|
||||
|
||||
Uses the LiteLLM SDK to call embedding models from providers such as OpenAI, Cohere, Azure, Bedrock, NVIDIA NIM, and other LiteLLM-supported backends. Requires the provider's API credentials.
|
||||
For the full option reference, provider setup examples, and live validation harness, see [LiteLLM Provider](litellm-provider.md).
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=litellm
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL=cohere/embed-english-v3.0
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS=1024
|
||||
export COHERE_API_KEY=...
|
||||
```
|
||||
|
||||
Basic Memory creates vector tables before the first embedding call, so non-default LiteLLM models must set `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS`. The LiteLLM OpenAI default (`openai/text-embedding-3-small`) uses 1536 dimensions automatically.
|
||||
|
||||
For fixed-size LiteLLM models, dimensions are used as Basic Memory's local vector schema and
|
||||
validation size. Basic Memory automatically sends dimensions as a provider-side output-size
|
||||
request for `text-embedding-3` model strings, where LiteLLM/OpenAI support reduced output
|
||||
dimensions. If an Azure/OpenAI deployment uses an arbitrary LiteLLM model string such as
|
||||
`azure/<deployment-name>` and the underlying model supports reduced dimensions, set
|
||||
`BASIC_MEMORY_SEMANTIC_EMBEDDING_FORWARD_DIMENSIONS=true`.
|
||||
|
||||
Some retrieval models are asymmetric: indexed passages and search queries must be embedded with different provider parameters. Basic Memory automatically sets LiteLLM `input_type` for known asymmetric model families:
|
||||
|
||||
- Cohere v3: documents use `search_document`, queries use `search_query`
|
||||
- NVIDIA NIM retrieval models: documents use `passage`, queries use `query`
|
||||
|
||||
For other asymmetric LiteLLM models, set the input types explicitly:
|
||||
|
||||
```bash
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_DOCUMENT_INPUT_TYPE=passage
|
||||
export BASIC_MEMORY_SEMANTIC_EMBEDDING_QUERY_INPUT_TYPE=query
|
||||
```
|
||||
|
||||
#### Live LiteLLM Validation
|
||||
|
||||
Provider APIs differ in subtle ways: some accept `dimensions`, some require separate
|
||||
document/query roles, and some route through deployment aliases that do not reveal the
|
||||
underlying model name. Before adding or changing LiteLLM model support, run the opt-in live
|
||||
evaluation harness:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export COHERE_API_KEY=...
|
||||
just test-litellm-live
|
||||
```
|
||||
|
||||
The built-in live cases cover:
|
||||
|
||||
| Case | Required key | What it validates |
|
||||
|---|---|---|
|
||||
| `openai/text-embedding-3-small` | `OPENAI_API_KEY` | Standard LiteLLM OpenAI embedding calls and normalized 1536-dimensional output. |
|
||||
| `cohere/embed-english-v3.0` | `COHERE_API_KEY` | Cohere v3 asymmetric `search_document` / `search_query` handling and fixed 1024-dimensional output. |
|
||||
|
||||
The harness embeds two documents and one query, checks vector dimensions and normalization,
|
||||
then verifies the authentication query ranks the authentication document above the distractor.
|
||||
It prints a table with per-model scores, norms, latency, role settings, and dimension-forwarding
|
||||
mode.
|
||||
|
||||
To validate provider aliases or additional LiteLLM backends, save custom JSON cases:
|
||||
|
||||
```bash
|
||||
export AZURE_API_KEY=...
|
||||
export AZURE_API_BASE=https://example.openai.azure.com
|
||||
export AZURE_API_VERSION=2024-02-01
|
||||
|
||||
cat > /tmp/litellm-azure-cases.json <<'JSON'
|
||||
[
|
||||
{
|
||||
"name": "azure-text-embedding-3-small-512",
|
||||
"model": "azure/<deployment-name>",
|
||||
"dimensions": 512,
|
||||
"api_key_env": "AZURE_API_KEY",
|
||||
"forward_dimensions": true
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
just test-litellm-live --cases-file /tmp/litellm-azure-cases.json
|
||||
```
|
||||
|
||||
NVIDIA NIM retrieval models can be checked the same way:
|
||||
|
||||
```bash
|
||||
export NVIDIA_NIM_API_KEY=...
|
||||
|
||||
cat > /tmp/litellm-nvidia-cases.json <<'JSON'
|
||||
[
|
||||
{
|
||||
"name": "nvidia-embed-qa-4",
|
||||
"model": "nvidia_nim/nvidia/embed-qa-4",
|
||||
"dimensions": 1024,
|
||||
"api_key_env": "NVIDIA_NIM_API_KEY",
|
||||
"document_input_type": "passage",
|
||||
"query_input_type": "query"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
just test-litellm-live --cases-file /tmp/litellm-nvidia-cases.json
|
||||
```
|
||||
|
||||
For repeatable local runs, put the same JSON array in a file and pass
|
||||
`just test-litellm-live --cases-file path/to/litellm-cases.json`.
|
||||
|
||||
When switching providers, models, dimensions, or LiteLLM document/query input types, rebuild embeddings:
|
||||
|
||||
```bash
|
||||
bm reindex --embeddings
|
||||
@@ -203,9 +313,10 @@ bm reindex -p my-project
|
||||
|
||||
- **Upgrade note**: Migration now performs a one-time automatic embedding backfill on upgrade.
|
||||
- **Manual enable case**: If you explicitly had `semantic_search_enabled=false` and then turn it on
|
||||
- **Provider change**: After switching between `fastembed` and `openai`
|
||||
- **Provider change**: After switching between `fastembed`, `openai`, and `litellm`
|
||||
- **Model change**: After changing `semantic_embedding_model`
|
||||
- **Dimension change**: After changing `semantic_embedding_dimensions`
|
||||
- **LiteLLM role change**: After changing `semantic_embedding_document_input_type` or `semantic_embedding_query_input_type`
|
||||
|
||||
The reindex command shows progress with embedded/skipped/error counts:
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from typing import Any, Callable
|
||||
from agent.memory_provider import MemoryProvider
|
||||
from tools.registry import tool_error
|
||||
|
||||
__version__ = "0.3.2"
|
||||
__version__ = "0.21.6"
|
||||
|
||||
logger = logging.getLogger("hermes.memory.basic-memory")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: basic-memory
|
||||
version: 0.3.2
|
||||
version: 0.21.6
|
||||
description: "Basic Memory — persistent knowledge graph backed by the basic-memory MCP server"
|
||||
pip_dependencies:
|
||||
- mcp
|
||||
|
||||
@@ -150,7 +150,7 @@ This plugin ships with workflow-oriented skills that are automatically loaded wh
|
||||
No manual installation needed. To update skills or install new ones as they become available:
|
||||
|
||||
```bash
|
||||
npx skills add basicmachines-co/basic-memory --path skills --agent openclaw
|
||||
npx skills add basicmachines-co/basic-memory/skills --agent openclaw
|
||||
```
|
||||
|
||||
See the canonical source at [`basic-memory/skills`](../../skills).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@basicmemory/openclaw-basic-memory",
|
||||
"version": "0.2.4",
|
||||
"version": "0.21.6",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"description": "Basic Memory plugin for OpenClaw — local-first knowledge graph for agent memory",
|
||||
"description": "Basic Memory plugin for OpenClaw \u2014 local-first knowledge graph for agent memory",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Basic Memory - Modern Command Runner
|
||||
|
||||
TESTMON_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_FLAGS", "--testmon-noselect")
|
||||
TESTMON_SELECT_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_SELECT_FLAGS", "--testmon --testmon-forceselect")
|
||||
TESTMON_REFRESH_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_REFRESH_FLAGS", "--testmon-noselect")
|
||||
|
||||
# Install dependencies
|
||||
install:
|
||||
uv sync
|
||||
@@ -35,40 +39,56 @@ test-sqlite: test-unit-sqlite test-int-sqlite
|
||||
test-postgres: test-unit-postgres test-int-postgres
|
||||
|
||||
# Run unit tests against SQLite
|
||||
test-unit-sqlite:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov tests
|
||||
test-unit-sqlite: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-sqlite tests
|
||||
|
||||
# Run unit tests against Postgres
|
||||
test-unit-postgres:
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov tests
|
||||
test-unit-postgres: testmon-seed
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=unit-postgres tests
|
||||
|
||||
# Run integration tests against SQLite (excludes semantic benchmarks — use just test-semantic)
|
||||
test-int-sqlite:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m "not semantic" test-int
|
||||
# Run integration tests against SQLite (excludes semantic tests and on-demand benchmarks —
|
||||
# use just test-semantic / run benchmark files explicitly)
|
||||
test-int-sqlite: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=int-sqlite -m "not semantic and not benchmark" test-int
|
||||
|
||||
# Run integration tests against Postgres
|
||||
# Note: Uses timeout due to FastMCP Client + asyncpg cleanup hang (tests pass, process hangs on exit)
|
||||
# See: https://github.com/jlowin/fastmcp/issues/1311
|
||||
test-int-postgres:
|
||||
test-int-postgres: testmon-seed
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Use gtimeout (macOS/Homebrew) or timeout (Linux)
|
||||
TIMEOUT_CMD=$(command -v gtimeout || command -v timeout || echo "")
|
||||
if [[ -n "$TIMEOUT_CMD" ]]; then
|
||||
$TIMEOUT_CMD --signal=KILL 600 bash -c 'BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov -m "not semantic" test-int' || test $? -eq 137
|
||||
$TIMEOUT_CMD --signal=KILL 600 bash -c 'BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=int-postgres -m "not semantic and not benchmark" test-int' || test $? -eq 137
|
||||
else
|
||||
echo "⚠️ No timeout command found, running without timeout..."
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov -m "not semantic" test-int
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=int-postgres -m "not semantic and not benchmark" test-int
|
||||
fi
|
||||
|
||||
# Run tests impacted by recent changes (requires pytest-testmon)
|
||||
# Pass paths or node ids after `just testmon` to limit the candidate set further.
|
||||
testmon *args:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov --testmon {{args}}
|
||||
testmon *args: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_SELECT_FLAGS}} --testmon-env=local {{args}}
|
||||
|
||||
# Seed pytest-testmon data into this worktree from the shared Git cache.
|
||||
testmon-seed:
|
||||
uv run python scripts/testmon_cache.py seed
|
||||
|
||||
# Refresh the shared pytest-testmon cache from a full backend test run.
|
||||
testmon-refresh:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
BASIC_MEMORY_TESTMON_FLAGS="{{TESTMON_REFRESH_FLAGS}}" just test
|
||||
uv run python scripts/testmon_cache.py refresh
|
||||
|
||||
# Show local and shared pytest-testmon cache locations.
|
||||
testmon-status:
|
||||
uv run python scripts/testmon_cache.py status
|
||||
|
||||
# Run MCP smoke test (fast end-to-end loop)
|
||||
test-smoke:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m smoke test-int/mcp/test_smoke_integration.py
|
||||
test-smoke: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=smoke -m smoke test-int/mcp/test_smoke_integration.py
|
||||
|
||||
# Fast local loop: lint, format, typecheck, impacted tests via pytest-testmon
|
||||
fast-check:
|
||||
@@ -97,27 +117,31 @@ postgres-migrate:
|
||||
# Run Windows-specific tests only (only works on Windows platform)
|
||||
# These tests verify Windows-specific database optimizations (locking mode, NullPool)
|
||||
# Will be skipped automatically on non-Windows platforms
|
||||
test-windows:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m windows tests test-int
|
||||
test-windows: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=windows -m windows tests test-int
|
||||
|
||||
# Run benchmark tests only (performance testing)
|
||||
# These are slow tests that measure sync performance with various file counts
|
||||
# Excluded from default test runs to keep CI fast
|
||||
test-benchmark:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m benchmark tests test-int
|
||||
test-benchmark: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=benchmark -m benchmark tests test-int
|
||||
|
||||
# Run semantic search quality benchmarks (all combos)
|
||||
test-semantic:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m semantic test-int/semantic/
|
||||
test-semantic: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=semantic -m semantic test-int/semantic/
|
||||
|
||||
# Run semantic benchmarks with JSON artifact output, then show report
|
||||
test-semantic-report:
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_BENCHMARK_OUTPUT=.benchmarks/semantic-quality.jsonl uv run pytest -p pytest_mock -v -s --no-cov -m semantic test-int/semantic/
|
||||
uv run python test-int/semantic/report.py .benchmarks/semantic-quality.jsonl
|
||||
|
||||
# Run opt-in live LiteLLM provider checks against configured external APIs
|
||||
test-litellm-live *args:
|
||||
BASIC_MEMORY_ENV=test BASIC_MEMORY_RUN_LITELLM_INTEGRATION=1 PYTHONPATH=test-int:src uv run python -m semantic.litellm_live_harness {{args}}
|
||||
|
||||
# Run semantic benchmarks (Postgres combos only)
|
||||
test-semantic-postgres:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov -m semantic -k postgres test-int/semantic/
|
||||
test-semantic-postgres: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=semantic-postgres -m semantic -k postgres test-int/semantic/
|
||||
|
||||
# View semantic benchmark results (rich formatted table)
|
||||
# Usage: just semantic-report [--filter-combo sqlite] [--filter-suite paraphrase] [--sort-by avg_latency_ms]
|
||||
@@ -133,8 +157,8 @@ benchmark-compare baseline candidate *args:
|
||||
|
||||
# Run all tests including Windows, Postgres, and Benchmarks (for CI/comprehensive testing)
|
||||
# Use this before releasing to ensure everything works across all backends and platforms
|
||||
test-all:
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov tests test-int
|
||||
test-all: testmon-seed
|
||||
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_FLAGS}} --testmon-env=all tests test-int
|
||||
|
||||
# Generate HTML coverage report
|
||||
coverage:
|
||||
@@ -265,8 +289,8 @@ check: lint format typecheck test
|
||||
# Run all code quality checks and all test suites, including semantic benchmarks
|
||||
check-all: lint format typecheck test test-semantic
|
||||
|
||||
# Validate every consolidated agent package (Claude Code, skills, Hermes, OpenClaw)
|
||||
package-check: package-check-claude-code package-check-skills package-check-hermes package-check-openclaw
|
||||
# Validate every consolidated agent package (Claude Code, Codex, skills, Hermes, OpenClaw)
|
||||
package-check: package-check-claude-code package-check-codex package-check-skills package-check-hermes package-check-openclaw
|
||||
|
||||
# Alias for plugin/package validation during consolidation work
|
||||
plugins-check: package-check
|
||||
@@ -278,6 +302,10 @@ agent-harness-check: package-check-claude-code package-check-hermes package-chec
|
||||
package-check-claude-code:
|
||||
just --justfile plugins/claude-code/justfile --working-directory plugins/claude-code check
|
||||
|
||||
# Codex plugin: manifest, bundled skills, hooks, MCP config, and schemas
|
||||
package-check-codex:
|
||||
just --justfile plugins/codex/justfile --working-directory plugins/codex check
|
||||
|
||||
# Shared top-level SKILL.md source
|
||||
package-check-skills:
|
||||
just --justfile skills/justfile --working-directory skills check
|
||||
@@ -303,7 +331,7 @@ set-version version scope="all":
|
||||
set-version-dry-run version scope="all":
|
||||
python3 scripts/update_versions.py "{{version}}" --scope "{{scope}}" --dry-run
|
||||
|
||||
# Set the version for just the plugin/agent artifacts (plugin, marketplaces, Hermes, OpenClaw)
|
||||
# Set the version for just the plugin/agent artifacts (plugins, marketplaces, Hermes, OpenClaw)
|
||||
set-packages-version version:
|
||||
just set-version "{{version}}" packages
|
||||
|
||||
@@ -365,6 +393,7 @@ release version:
|
||||
.claude-plugin/marketplace.json \
|
||||
plugins/claude-code/.claude-plugin/plugin.json \
|
||||
plugins/claude-code/.claude-plugin/marketplace.json \
|
||||
plugins/codex/.codex-plugin/plugin.json \
|
||||
integrations/hermes/plugin.yaml \
|
||||
integrations/hermes/__init__.py \
|
||||
integrations/openclaw/package.json
|
||||
@@ -438,6 +467,7 @@ beta version:
|
||||
.claude-plugin/marketplace.json \
|
||||
plugins/claude-code/.claude-plugin/plugin.json \
|
||||
plugins/claude-code/.claude-plugin/marketplace.json \
|
||||
plugins/codex/.codex-plugin/plugin.json \
|
||||
integrations/hermes/plugin.yaml \
|
||||
integrations/hermes/__init__.py \
|
||||
integrations/openclaw/package.json
|
||||
|
||||
@@ -6,18 +6,24 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official plugins from Basic Machines for knowledge management and AI-assisted development",
|
||||
"version": "0.3.13"
|
||||
"version": "0.21.6"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "basic-memory",
|
||||
"source": "./",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph — session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.3.13",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph \u2014 session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.21.6",
|
||||
"author": {
|
||||
"name": "Basic Machines"
|
||||
},
|
||||
"keywords": ["memory", "knowledge", "mcp", "specs", "context"]
|
||||
"keywords": [
|
||||
"memory",
|
||||
"knowledge",
|
||||
"mcp",
|
||||
"specs",
|
||||
"context"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "basic-memory",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph — session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.3.13",
|
||||
"description": "The bridge between Claude's working memory and Basic Memory's durable knowledge graph \u2014 session briefings, pre-compaction checkpoints, and capture reflexes",
|
||||
"version": "0.21.6",
|
||||
"author": {
|
||||
"name": "Basic Machines"
|
||||
},
|
||||
|
||||
@@ -15,26 +15,26 @@ Memory's durable graph**, rather than a memory layer of its own. See
|
||||
(`my-team/notes`) or `external_id` UUIDs, since project names collide across
|
||||
workspaces. Reads route over the user's OAuth session; capture **never** writes to a
|
||||
shared project.
|
||||
- **`/basic-memory:share <note>`** (`skills/share/`) — the deliberate personal→team
|
||||
- **`/basic-memory:bm-share <note>`** (`skills/bm-share/`) — the deliberate personal→team
|
||||
write: copies a note from the primary project into a configured `teamProjects`
|
||||
target's `promoteFolder`, with `shared_from` attribution and a confirmation step.
|
||||
Preserves the note's type so shared decisions stay findable in the team's structured
|
||||
recall. (Phase 4)
|
||||
- **`/basic-memory:setup`** (`skills/setup/`) — a short guided interview that
|
||||
- **`/basic-memory:bm-setup`** (`skills/bm-setup/`) — a short guided interview that
|
||||
configures the project for the plugin: maps it to a Basic Memory project (picking
|
||||
an existing one or creating a new one), seeds the `session`/`decision`/`task`
|
||||
schemas into the project, installs the shared `memory-*` skills via
|
||||
`npx skills add basicmachines-co/basic-memory --path skills` (the plugin doesn't
|
||||
`npx skills add basicmachines-co/basic-memory/skills` (the plugin doesn't
|
||||
vendor its own copies — `skills/` is the single source of truth, shared with
|
||||
OpenClaw), optionally learns the project's placement conventions, and enables the
|
||||
capture reflexes. Writes the `basicMemory` block to
|
||||
`.claude/settings.json` (or `settings.local.json`). The SessionStart hook nudges
|
||||
toward this on first run; running it (writing the config) stops the nudge. (Phase 3)
|
||||
- **`/basic-memory:remember <text>`** (`skills/remember/`) — quick deliberate
|
||||
- **`/basic-memory:bm-remember <text>`** (`skills/bm-remember/`) — quick deliberate
|
||||
capture. Writes the text verbatim to the `rememberFolder` (default `bm-remember`)
|
||||
with a first-line title and a `manual-capture` tag, via the connected Basic Memory
|
||||
MCP server. Also fires when the user says "remember that…". (Phase 2)
|
||||
- **`/basic-memory:status`** (`skills/status/`) — diagnostic that reports the active
|
||||
- **`/basic-memory:bm-status`** (`skills/bm-status/`) — diagnostic that reports the active
|
||||
project, capture/remember folders, output-style state, recent session checkpoints,
|
||||
and active-task count. User-invoked only (`disable-model-invocation`). (Phase 2)
|
||||
|
||||
@@ -62,7 +62,7 @@ Memory's durable graph**, rather than a memory layer of its own. See
|
||||
|
||||
### Changed
|
||||
|
||||
- **SessionStart hook now nudges toward `/basic-memory:setup` on first run** — when
|
||||
- **SessionStart hook now nudges toward `/basic-memory:bm-setup` on first run** — when
|
||||
no `basicMemory` config block is present in either settings file. The nudge
|
||||
survives a failed/empty task query (so a brand-new user with no project yet still
|
||||
sees it), and stops once setup writes the config. (Phase 3)
|
||||
@@ -84,7 +84,7 @@ Memory's durable graph**, rather than a memory layer of its own. See
|
||||
|
||||
### Notes
|
||||
|
||||
- Slash commands shipped by later phases (`/basic-memory:setup`,
|
||||
- Slash commands shipped by later phases (`/basic-memory:bm-setup`,
|
||||
`:remember`, `:status`) will be **plugin-namespaced** — Claude Code namespaces
|
||||
all plugin skills as `/<plugin>:<skill>`.
|
||||
- Requires `basic-memory >= 0.19.0` (for `metadata_filters` / structured recall).
|
||||
|
||||
@@ -69,9 +69,9 @@ plugins/claude-code/
|
||||
│ └── basic-memory.md # reflexes: search first, capture decisions
|
||||
│ # NOTE: rules/ deferred — path-scoped rules don't load yet (Q5)
|
||||
├── skills/
|
||||
│ ├── setup/SKILL.md # /basic-memory:setup — bootstrap interview (first-run)
|
||||
│ ├── remember/SKILL.md # /basic-memory:remember <text> — quick deliberate capture
|
||||
│ └── status/SKILL.md # /basic-memory:status — show plugin state
|
||||
│ ├── bm-setup/SKILL.md # /basic-memory:bm-setup — bootstrap interview (first-run)
|
||||
│ ├── bm-remember/SKILL.md # /basic-memory:bm-remember <text> — quick deliberate capture
|
||||
│ └── bm-status/SKILL.md # /basic-memory:bm-status — show plugin state
|
||||
├── schemas/ # picoschema seeds, copied into the user's BM project at bootstrap
|
||||
│ ├── session.md # type: session — resume checkpoints
|
||||
│ ├── decision.md # type: decision — durable choices + rationale
|
||||
@@ -87,7 +87,7 @@ Three layers, matching what Hermes and OpenClaw converged on:
|
||||
| ----------- | ---------------------------------- | ------------------------------------------ | ------------------------------------- |
|
||||
| Ambient | `hooks/`, `rules/` | Lifecycle events, file context | Brief Claude, checkpoint, guide placement |
|
||||
| Background | `output-styles/basic-memory.md` | System prompt, every turn | Reflexes — search first, capture inline |
|
||||
| Deliberate | `skills/{setup,remember,status}/` | User invokes (`/basic-memory:setup`, `/basic-memory:remember`) | One-shot user gestures |
|
||||
| Deliberate | `skills/{bm-setup,bm-remember,bm-status}/` | User invokes (`/basic-memory:bm-setup`, `/basic-memory:bm-remember`) | One-shot user gestures |
|
||||
|
||||
## 4. The core flows
|
||||
|
||||
@@ -191,13 +191,17 @@ If/when path-scoped rules start working *and* support out-of-tree globs, we can
|
||||
|
||||
Three skills only, each Claude-Code-specific (everything else lives in top-level `skills/`).
|
||||
|
||||
> **Verified (Q3) — slash commands are always plugin-namespaced.** A skill folder `skills/remember/` in a plugin whose `plugin.json` name is `basic-memory` is invoked as **`/basic-memory:remember`**, not `/remember` — namespacing is mandatory and can't be shortened. Consequence: we drop the redundant `bm-` prefix from skill folder names (the namespace already says `basic-memory`). So the folders are `setup/`, `remember/`, `status/`, surfacing as `/basic-memory:setup`, `/basic-memory:remember`, `/basic-memory:status`. Skills are auto-discovered on install — no extra registration.
|
||||
> **Verified (Q3) — slash commands are always plugin-namespaced.** A skill folder `skills/bm-setup/` in a plugin named `basic-memory` is invoked as **`/basic-memory:bm-setup`** — namespacing is mandatory and can't be shortened. Skills are auto-discovered on install, no extra registration.
|
||||
>
|
||||
> **Naming decision (revised after dogfood).** The four skills carry a **`bm-` prefix** (`bm-setup`, `bm-remember`, `bm-share`, `bm-status`). Phase 2 originally *dropped* the prefix, reasoning that the `basic-memory:` namespace already disambiguates. Dogfooding the merged plugin proved otherwise: the Claude Code slash **picker shows only the bare skill name** — the plugin name is relegated to the focused-item tooltip — so un-prefixed skills blend into the list with other plugins' similarly-named ones (`spec`, `simplify`, `schedule`, …). The `bm-` prefix makes them group and read as ours in the picker. The cost is a cosmetic stutter in the full form (`/basic-memory:bm-setup`).
|
||||
>
|
||||
> **This is a workaround, not the end state.** The real fix is upstream: [anthropics/claude-code#50486](https://github.com/anthropics/claude-code/issues/50486) (open) asks the picker to namespace plugin skills the way it already does for *commands*. If/when that lands, the picker would show `basic-memory:setup` natively — revisit dropping the `bm-` prefix then, to kill the stutter. (Aside: [#22063](https://github.com/anthropics/claude-code/issues/22063) — a `name` frontmatter field stripping the namespace — does **not** affect us; with `name == dir` the namespace is retained, confirmed live in the session skill list.)
|
||||
|
||||
**`/basic-memory:setup`** — bootstrap interview (§7). Run after install and any time the user wants to reconfigure.
|
||||
**`/basic-memory:bm-setup`** — bootstrap interview (§7). Run after install and any time the user wants to reconfigure.
|
||||
|
||||
**`/basic-memory:remember <text>`** — quick capture. Writes to a `bm-remember/` folder, separated from auto-captures. First line becomes title (truncated to 80 chars), tagged `manual-capture`. Optional `--project` flag for cross-project.
|
||||
**`/basic-memory:bm-remember <text>`** — quick capture. Writes to a `bm-remember/` folder, separated from auto-captures. First line becomes title (truncated to 80 chars), tagged `manual-capture`. Optional `--project` flag for cross-project.
|
||||
|
||||
**`/basic-memory:status`** — show plugin state: active BM project, capture folders, recent SessionNotes, sync status, last successful BM call. Trust-building UI.
|
||||
**`/basic-memory:bm-status`** — show plugin state: active BM project, capture folders, recent SessionNotes, sync status, last successful BM call. Trust-building UI.
|
||||
|
||||
### 4.5 The schema layer — why our note types are contracts, not conventions
|
||||
|
||||
@@ -207,8 +211,8 @@ Basic Memory ships a [schema system](https://docs.basicmemory.com/raw/concepts/s
|
||||
|
||||
| Note type | `type:` | Written by | Purpose |
|
||||
| --------- | ------- | ---------- | ------- |
|
||||
| Session | `session` | PreCompact hook, `/basic-memory:handoff` (future) | Resume cursor — what we were doing, what's next |
|
||||
| Decision | `decision` | output-style reflex, `/basic-memory:decide` (future) | Durable record of choices + rationale |
|
||||
| Session | `session` | PreCompact hook, `/basic-memory:bm-handoff` (future) | Resume cursor — what we were doing, what's next |
|
||||
| Decision | `decision` | output-style reflex, `/basic-memory:bm-decide` (future) | Durable record of choices + rationale |
|
||||
| Task | `task` | user + Claude | Active work tracking (aligns with `skills/memory-tasks`) |
|
||||
|
||||
These conform to the SessionNote / DecisionNote picoschema shapes defined in SPEC-55, so when the SPEC-55 Writer SDK and async pipeline land, validation Just Works and nothing has to change in user-facing behavior.
|
||||
@@ -264,7 +268,7 @@ These are orthogonal concepts that the plugin must explicitly map.
|
||||
### 5.1 Mapping model
|
||||
|
||||
Each Claude Code project has:
|
||||
- **One primary BM project** — destination for SessionStart context + PreCompact checkpoints + `/basic-memory:remember`. Required.
|
||||
- **One primary BM project** — destination for SessionStart context + PreCompact checkpoints + `/basic-memory:bm-remember`. Required.
|
||||
- **Zero or more secondary BM projects** — read-only by default for recall (SessionStart can query them); writes require explicit user gesture.
|
||||
|
||||
Mapping is configured in `.claude/settings.json`:
|
||||
@@ -311,11 +315,11 @@ UUID and routes accordingly. Cross-workspace reads route over the user's OAuth s
|
||||
### 6.1 Defaults — safe by design
|
||||
|
||||
- **Auto-capture defaults to personal.** SessionNotes, PreCompact checkpoints, and
|
||||
`/basic-memory:remember` quick captures **only ever** land in `primaryProject`. The
|
||||
`/basic-memory:bm-remember` quick captures **only ever** land in `primaryProject`. The
|
||||
capture hooks never write to a shared project — full stop, no opt-in flag in v0.4.
|
||||
- **Recall reads across.** SessionStart queries the shared projects in parallel for
|
||||
open decisions and folds them into the brief (read-only — discloses nothing).
|
||||
- **Sharing is a deliberate gesture.** `/basic-memory:share` copies a personal note
|
||||
- **Sharing is a deliberate gesture.** `/basic-memory:bm-share` copies a personal note
|
||||
into a configured team project with attribution, after explicit confirmation. The
|
||||
personal→team boundary is always a visible, manual action.
|
||||
|
||||
@@ -334,7 +338,7 @@ UUID and routes accordingly. Cross-workspace reads route over the user's OAuth s
|
||||
```
|
||||
|
||||
- `secondaryProjects` — workspace-qualified refs (or UUIDs) read for recall. Read-only.
|
||||
- `teamProjects` — share targets for `/basic-memory:share`; each carries a
|
||||
- `teamProjects` — share targets for `/basic-memory:bm-share`; each carries a
|
||||
`promoteFolder` (default `shared`). Also read for recall (SessionStart reads the
|
||||
union of `secondaryProjects` and `teamProjects` keys, capped at 6 per session).
|
||||
|
||||
@@ -349,15 +353,15 @@ shared session memory; until then we don't ship a flag we don't enforce.
|
||||
- New team members get instant context — first SessionStart pulls the team graph and they're already oriented.
|
||||
- Cross-pollination — operator running a strategy session sees recent technical decisions from builders.
|
||||
|
||||
## 7. Bootstrap — `/basic-memory:setup` interview
|
||||
## 7. Bootstrap — `/basic-memory:bm-setup` interview
|
||||
|
||||
Users get overwhelmed starting from zero. The plugin opens with a guided interview that establishes opinionated defaults from a short conversation.
|
||||
|
||||
> **Verified (Q6) — there is no install hook.** Claude Code has no PostInstall/PreInstall lifecycle event (feature request [#11240](https://github.com/anthropics/claude-code/issues/11240) was closed as duplicate). We can't auto-run setup the moment the plugin installs. The workaround is the **SessionStart hook detecting first-run**: it checks for a sentinel (e.g. `${CLAUDE_PLUGIN_DATA}/.bootstrapped` or the absence of a `basicMemory` config) and, if missing, injects a one-line nudge — *"Basic Memory isn't configured yet. Run `/basic-memory:setup` (≈3 min) to wire it up."* A bonus verified capability: SessionStart can return `{"reloadSkills": true}` to re-scan skills mid-session, useful if setup writes new skills/config that should activate without a restart.
|
||||
> **Verified (Q6) — there is no install hook.** Claude Code has no PostInstall/PreInstall lifecycle event (feature request [#11240](https://github.com/anthropics/claude-code/issues/11240) was closed as duplicate). We can't auto-run setup the moment the plugin installs. The workaround is the **SessionStart hook detecting first-run**: it checks for a sentinel (e.g. `${CLAUDE_PLUGIN_DATA}/.bootstrapped` or the absence of a `basicMemory` config) and, if missing, injects a one-line nudge — *"Basic Memory isn't configured yet. Run `/basic-memory:bm-setup` (≈3 min) to wire it up."* A bonus verified capability: SessionStart can return `{"reloadSkills": true}` to re-scan skills mid-session, useful if setup writes new skills/config that should activate without a restart.
|
||||
|
||||
**Trigger paths:**
|
||||
1. SessionStart detects no `basicMemory` config and no `basic-memory` note → inject the one-line nudge suggesting `/basic-memory:setup` (we cannot run it automatically)
|
||||
2. User runs `/basic-memory:setup` explicitly (anytime, including for reconfiguration)
|
||||
1. SessionStart detects no `basicMemory` config and no `basic-memory` note → inject the one-line nudge suggesting `/basic-memory:bm-setup` (we cannot run it automatically)
|
||||
2. User runs `/basic-memory:bm-setup` explicitly (anytime, including for reconfiguration)
|
||||
|
||||
**Interview script** (Claude executes it; SKILL.md provides the structure):
|
||||
|
||||
@@ -368,14 +372,14 @@ Users get overwhelmed starting from zero. The plugin opens with a guided intervi
|
||||
3. *"Are you using Basic Memory Cloud or local-only?"*
|
||||
- If cloud + team workspace exists: *"You're on the `<team>` workspace. Want me to also read from team projects for recall? (read-only by default; we can opt-in to writes later)"*
|
||||
4. *"How chatty should I be?"*
|
||||
- **Light** (default): SessionStart brief on each session, PreCompact checkpoint, `/basic-memory:remember` on demand
|
||||
- **Light** (default): SessionStart brief on each session, PreCompact checkpoint, `/basic-memory:bm-remember` on demand
|
||||
- **Standard**: above + capture decisions inline via output-style
|
||||
- **Heavy**: above + every-session SessionNote even without compaction
|
||||
5. *"Should I look at your existing notes and suggest some placement conventions?"* (yes → runs `schema_infer` on the existing notes, summarizes the patterns it found, and stores them in the `basicMemory` settings block — see §4.3, since path-scoped rules don't load yet — from the user's *real* conventions rather than imposed ones)
|
||||
6. *"I'll set up schemas for session checkpoints and decisions so I can find them precisely later — okay?"* (yes → writes `schemas/session.md`, `schemas/decision.md`, `schemas/task.md` into the primary project, skipping any that already exist; validation mode `warn`)
|
||||
7. *"Want me to enable the `basic-memory` output style now?"* (yes → adds `outputStyle: basic-memory` to settings)
|
||||
|
||||
**Output:** writes `.claude/settings.json` (with prompt to commit or keep local) including any inferred conventions, writes the three schema notes, optionally creates the BM project, and drops the bootstrap sentinel so SessionStart stops nudging. Closes with: *"Done. I'll start using this on the next message. Try `/basic-memory:status` anytime to see what I'm tracking."*
|
||||
**Output:** writes `.claude/settings.json` (with prompt to commit or keep local) including any inferred conventions, writes the three schema notes, optionally creates the BM project, and drops the bootstrap sentinel so SessionStart stops nudging. Closes with: *"Done. I'll start using this on the next message. Try `/basic-memory:bm-status` anytime to see what I'm tracking."*
|
||||
|
||||
**Why an interview, not a config form:** the interview is *adaptive* — it skips questions when context is obvious (e.g., it sees you're on cloud and on a team; doesn't ask about local), suggests reasonable defaults the user can accept with a single word, and produces a meaningful starting point in under 3 minutes. A config form makes the user own every decision.
|
||||
|
||||
@@ -431,10 +435,10 @@ Verified 2026-05-28 against Claude Code v2.1.153 and basic-memory 0.21.5, via a
|
||||
|---|----------|---------|--------|--------------------|
|
||||
| **Q1** | Does SessionStart fire before/after auto-memory loads? Can the hook use auto-memory as input? | **uncertain** | Firing order vs `MEMORY.md` load is **not documented**. The often-cited "SessionStart fires before CLAUDE.md loads" phrasing isn't in current docs. What *is* certain: the hook can read `MEMORY.md` from disk anytime. | Don't assume auto-memory is in context at SessionStart. If the brief wants it, **read the file from disk** (§4.1). Don't build anything that depends on context-load ordering. |
|
||||
| **Q2** | Does PreCompact block synchronously? Timeout? Can it do multi-second/LLM work? | **confirmed** | Blocks synchronously; **600s default timeout** (configurable). MCP/LLM calls fit easily. On timeout the hook is killed and compaction proceeds. (To *block* compaction you'd return exit 2 / `decision:block` before the timeout — we don't.) | **Upgraded the design.** `preCompactCapture` default is now `"summarized"` (real LLM pass), not extractive (§4.2, §8). Write the note early, enrich after, in case of kill. |
|
||||
| **Q3** | Do plugin skills/commands appear in the `/` menu, and as what? | **confirmed** | Auto-discovered on install, but **always namespaced** as `/<plugin-name>:<skill>`. Can't be shortened. No sparse/subdir caveats. | Drop the `bm-` prefix; folders become `setup/`, `remember/`, `status/` → `/basic-memory:setup` etc. (§4.4). README must show the namespaced form. |
|
||||
| **Q3** | Do plugin skills/commands appear in the `/` menu, and as what? | **confirmed** | Auto-discovered, **always namespaced** as `/<plugin-name>:<skill>`, but the picker shows only the bare skill name (namespace in the tooltip). | Use a `bm-` prefix (`bm-setup` …) so they're legible in the picker — see §4.4. Workaround for [anthropics/claude-code#50486](https://github.com/anthropics/claude-code/issues/50486); revisit if that lands. |
|
||||
| **Q4** | How does SessionStart inject context? Size limit? | **confirmed** | Plain stdout (added to context, no JSON needed) **or** JSON `hookSpecificOutput.additionalContext`. **10,000-char cap** per string; overflow spills to a file. Shell-profile echoes corrupt output. | Use plain stdout. Keep the brief well under 10k — cap each section's item count, prefer permalinks over previews. Guard against shell-profile noise (§4.1). |
|
||||
| **Q5** | Do path-scoped `rules/` with `paths:` load for BM files (incl. outside the git tree)? | **refuted** | Path-scoped rules **don't load automatically at all** — open bug [#16853](https://github.com/anthropics/claude-code/issues/16853) ("never worked"). Even when fixed they're repo-relative (won't match `~/basic-memory/`, [#25562](https://github.com/anthropics/claude-code/issues/25562)). | **Dropped `rules/` from the plugin.** Placement/format conventions move to the `basicMemory` settings block + SessionStart brief + output-style (§4.3). Revisit if the platform bug is fixed *and* out-of-tree globs are supported. |
|
||||
| **Q6** | Is there a run-on-install hook for bootstrap? | **confirmed** | No PostInstall/PreInstall lifecycle event ([#11240](https://github.com/anthropics/claude-code/issues/11240) closed as dup). Only SessionStart + explicit Setup. Bonus: SessionStart can return `{"reloadSkills": true}`. | Bootstrap can't auto-run. SessionStart detects first-run via a **sentinel file** and nudges the user to run `/basic-memory:setup` (§7). |
|
||||
| **Q6** | Is there a run-on-install hook for bootstrap? | **confirmed** | No PostInstall/PreInstall lifecycle event ([#11240](https://github.com/anthropics/claude-code/issues/11240) closed as dup). Only SessionStart + explicit Setup. Bonus: SessionStart can return `{"reloadSkills": true}`. | Bootstrap can't auto-run. SessionStart detects first-run via a **sentinel file** and nudges the user to run `/basic-memory:bm-setup` (§7). |
|
||||
| **Q7** | Does `search_notes` support `metadata_filters` + `after_date` in 0.21.5? Min version? | **confirmed** | Both present and working in 0.21.5. Full operator set: `$in`, `$gt/$gte/$lt/$lte`, `$between`, array-contains, equality, dot-notation (`schema.confidence`). On `search_notes` since **v0.18.1** (verify corrected the first pass's "v0.19.0"). | Structured recall is safe as a **baseline** — no fallback path needed for the 0.21.5 target. Pin a minimum `basic-memory >= 0.19.0` in prerequisites for margin. Use `tags`/`status` shorthands for common cases (§4.1). |
|
||||
| **Q8** | Is `schema_validate` cheap enough for PreCompact? Are the schema tools in 0.21.5? | **confirmed** | All three (`validate`/`infer`/`diff`) present since v0.19.0. `schema_validate(identifier=…)` = **cheap single-note**. `schema_validate(note_type=…)` = **batch, O(N)** with per-note file I/O. | PreCompact validates only the note it just wrote via the **identifier path** (§4.2). Batch validation + `schema_diff` go to the nightly hygiene routine (§13). |
|
||||
|
||||
@@ -495,7 +499,7 @@ eventual default is `"summarized"` — the LLM pass is the first enrich step.
|
||||
schemas / output-style / settings carry no version)
|
||||
|
||||
**Carried into later phases (not part of the minimal cut):** the first-run *sentinel
|
||||
nudge* moves to Phase 3 (it should point at `/basic-memory:setup`, which doesn't exist
|
||||
nudge* moves to Phase 3 (it should point at `/basic-memory:bm-setup`, which doesn't exist
|
||||
yet); the multi-query parallel brief and the LLM-summarized PreCompact are the enrich
|
||||
steps.
|
||||
|
||||
@@ -517,10 +521,10 @@ bash-injection scripts — avoids shell-quoting fragility on arbitrary user text
|
||||
the `${CLAUDE_SKILL_DIR}` path uncertainty, and works regardless of the MCP server's
|
||||
tool-name prefix.
|
||||
|
||||
- [x] Write `skills/remember/SKILL.md` → `/basic-memory:remember` — model-invocable
|
||||
- [x] Write `skills/bm-remember/SKILL.md` → `/basic-memory:bm-remember` — model-invocable
|
||||
("remember that…"); writes verbatim to `rememberFolder` with a first-line title and
|
||||
`manual-capture` tag via `write_note`.
|
||||
- [x] Write `skills/status/SKILL.md` → `/basic-memory:status` — `disable-model-invocation`
|
||||
- [x] Write `skills/bm-status/SKILL.md` → `/basic-memory:bm-status` — `disable-model-invocation`
|
||||
(user-only diagnostic); reports project, folders, output-style, recent checkpoints,
|
||||
active-task count.
|
||||
- [x] Test slash-command discovery end-to-end — installed from a local marketplace and
|
||||
@@ -534,7 +538,7 @@ tool-name prefix.
|
||||
Implemented as a **prose skill** (the interview is conversational; Claude runs it
|
||||
using its MCP tools). Verified the whole loop end-to-end against throwaway projects.
|
||||
|
||||
- [x] Write `skills/setup/SKILL.md` → `/basic-memory:setup` — adaptive interview that
|
||||
- [x] Write `skills/bm-setup/SKILL.md` → `/basic-memory:bm-setup` — adaptive interview that
|
||||
maps the project, seeds schemas, optionally learns conventions, writes settings, and
|
||||
enables the output style. Model-invocable ("set up basic memory") + user-invocable.
|
||||
- [x] Wire `schema_infer`/`list_directory` into the bootstrap — the skill inspects the
|
||||
@@ -546,7 +550,7 @@ using its MCP tools). Verified the whole loop end-to-end against throwaway proje
|
||||
`schema_validate` (`entity=Session/Decision/Task`). This corrects the earlier Phase 1
|
||||
finding — schema seeding is a plain content copy; the previous "must use
|
||||
`note_type`/`metadata`" conclusion was confounded by the enum YAML bug.
|
||||
- [x] First-run detection in SessionStart — nudges toward `/basic-memory:setup` when no
|
||||
- [x] First-run detection in SessionStart — nudges toward `/basic-memory:bm-setup` when no
|
||||
`basicMemory` config block exists (config presence is the sentinel; no separate file).
|
||||
The nudge survives a failed/empty task query. Verified across all three config states
|
||||
(no config → nudge; block without project → pin tip; project pinned → silent).
|
||||
@@ -561,7 +565,7 @@ using its MCP tools). Verified the whole loop end-to-end against throwaway proje
|
||||
### Phase 4: Team workspace support — ✅ DONE (2026-05-28)
|
||||
|
||||
Grounded in a real two-workspace BM Cloud account (verified name-collision routing,
|
||||
OAuth cross-workspace reads). Pulled `/basic-memory:share` forward from future-work
|
||||
OAuth cross-workspace reads). Pulled `/basic-memory:bm-share` forward from future-work
|
||||
since team usage needs a safe write path.
|
||||
|
||||
- [x] Extend SessionStart to read primary + shared projects **in parallel** —
|
||||
@@ -569,7 +573,7 @@ since team usage needs a safe write path.
|
||||
decisions; each shared project: open decisions). Routes by qualified name or UUID,
|
||||
per-call timeout, capped at 6 shared projects, graceful on any failure. Verified
|
||||
against the real `my-team-2` workspace and with local fixtures.
|
||||
- [x] Add `/basic-memory:share` (`skills/share/`) — the deliberate personal→team
|
||||
- [x] Add `/basic-memory:bm-share` (`skills/bm-share/`) — the deliberate personal→team
|
||||
write: reads a note from `primaryProject`, confirms, and copies it to a configured
|
||||
`teamProjects` target's `promoteFolder` with `shared_from` attribution. Preserves
|
||||
the note's type so shared decisions stay findable in the team's structured recall.
|
||||
@@ -611,11 +615,11 @@ Docs done 2026-05-28; dogfood is the remaining (human) step.
|
||||
## 13. Future work (post-v0.4)
|
||||
|
||||
- **Routines integration** — three routine templates (nightly hygiene, weekly digest, daily reflection). Separate design doc. The nightly hygiene routine is the natural home for `schema_diff` drift detection and the deferred LLM-summary pass over the day's extractive SessionNotes.
|
||||
- ~~**`/basic-memory:share`** — promote personal note → team project~~ — shipped in Phase 4.
|
||||
- ~~**`/basic-memory:bm-share`** — promote personal note → team project~~ — shipped in Phase 4.
|
||||
- **Team `autoWrite`** — opt-in for auto-capture (PreCompact/remember) to write to a
|
||||
team project, for teams that want shared session memory. Deferred from Phase 4 (§6.2).
|
||||
- **`/basic-memory:blame <sha>`** — code archaeology, builder add-on.
|
||||
- **`/basic-memory:bm-blame <sha>`** — code archaeology, builder add-on.
|
||||
- **Commit-hook integration** — PostToolUse on `Bash(git commit *)` writes CommitNote linking SHA to session's BM writes.
|
||||
- **Subagent memory bundling** — explore `memory: project|user` on dedicated BM subagents.
|
||||
- **Statusline** — small visible presence (active project, last write).
|
||||
- **`/basic-memory:promote`** — review auto-memory MEMORY.md, graduate observations into BM with proper schema.
|
||||
- **`/basic-memory:bm-promote`** — review auto-memory MEMORY.md, graduate observations into BM with proper schema.
|
||||
|
||||
@@ -38,10 +38,10 @@ Plugin skills are namespaced under the plugin name:
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/basic-memory:setup` | One-time guided setup — maps the project to a Basic Memory project, seeds the note schemas, installs the shared `memory-*` skills, optionally learns your conventions, and turns on the capture reflexes. Run this first. |
|
||||
| `/basic-memory:remember <text>` | Quick capture — saves the text to the `bm-remember` folder with a `manual-capture` tag. Also fires when you say "remember that…". |
|
||||
| `/basic-memory:share <note>` | Promote a personal note to a configured team project, with attribution and confirmation. The deliberate way to write to a shared workspace. |
|
||||
| `/basic-memory:status` | Diagnostic — shows the active project, team read-sources and share targets, capture folders, output-style state, recent session checkpoints, and active-task count. |
|
||||
| `/basic-memory:bm-setup` | One-time guided setup — maps the project to a Basic Memory project, seeds the note schemas, installs the shared `memory-*` skills, optionally learns your conventions, and turns on the capture reflexes. Run this first. |
|
||||
| `/basic-memory:bm-remember <text>` | Quick capture — saves the text to the `bm-remember` folder with a `manual-capture` tag. Also fires when you say "remember that…". |
|
||||
| `/basic-memory:bm-share <note>` | Promote a personal note to a configured team project, with attribution and confirmation. The deliberate way to write to a shared workspace. |
|
||||
| `/basic-memory:bm-status` | Diagnostic — shows the active project, team read-sources and share targets, capture folders, output-style state, recent session checkpoints, and active-task count. |
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -61,7 +61,7 @@ claude plugin install basic-memory@basicmachines-co
|
||||
|
||||
## Configuration
|
||||
|
||||
The fastest path is **`/basic-memory:setup`** — a ~2-minute interview that writes
|
||||
The fastest path is **`/basic-memory:bm-setup`** — a ~2-minute interview that writes
|
||||
the config, seeds the schemas, and turns on the capture reflexes. The SessionStart
|
||||
hook nudges you toward it on first run.
|
||||
|
||||
@@ -103,14 +103,14 @@ ever auto-writing to the shared graph.**
|
||||
- **Read across** — add team projects to `secondaryProjects`. SessionStart pulls their
|
||||
open decisions into your brief (in parallel, read-only), so you start oriented on
|
||||
what the team has decided.
|
||||
- **Capture stays personal** — session checkpoints and `/basic-memory:remember` only
|
||||
- **Capture stays personal** — session checkpoints and `/basic-memory:bm-remember` only
|
||||
ever write to your `primaryProject`. Nothing lands in a team project automatically.
|
||||
- **Share deliberately** — `/basic-memory:share` copies a chosen note into a
|
||||
- **Share deliberately** — `/basic-memory:bm-share` copies a chosen note into a
|
||||
`teamProjects` target (with attribution and a confirmation step). That's the only
|
||||
path to a shared write.
|
||||
|
||||
Because project names repeat across workspaces, team refs must be **workspace-qualified**
|
||||
(`my-team/notes`) or `external_id` UUIDs — `/basic-memory:setup` fills these in for you
|
||||
(`my-team/notes`) or `external_id` UUIDs — `/basic-memory:bm-setup` fills these in for you
|
||||
from `list_workspaces`.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -38,7 +38,7 @@ flowchart TB
|
||||
OS["output-style<br/>→ search-first / capture / cite reflexes"]
|
||||
end
|
||||
subgraph Deliberate["Deliberate (slash commands)"]
|
||||
SK["/basic-memory:setup · remember · share · status"]
|
||||
SK["/basic-memory:bm-setup · bm-remember · bm-share · bm-status"]
|
||||
end
|
||||
|
||||
Ambient --> MCP["Basic Memory MCP server"]
|
||||
@@ -85,7 +85,7 @@ Key properties:
|
||||
is ~one query, not the sum.
|
||||
- **Best-effort.** No Basic Memory, no config, or a slow cloud read never blocks or
|
||||
errors the session — the worst case is a missing or partial brief.
|
||||
- **First-run aware.** With no config it nudges toward `/basic-memory:setup`.
|
||||
- **First-run aware.** With no config it nudges toward `/basic-memory:bm-setup`.
|
||||
|
||||
## PreCompact — the checkpoint
|
||||
|
||||
@@ -145,7 +145,7 @@ flowchart TB
|
||||
|
||||
T1 -- "read-only<br/>(SessionStart)" --> P
|
||||
T2 -- "read-only<br/>(SessionStart)" --> P
|
||||
P -- "/basic-memory:share<br/>(deliberate, confirmed)" --> T2
|
||||
P -- "/basic-memory:bm-share<br/>(deliberate, confirmed)" --> T2
|
||||
|
||||
note["Auto-capture (checkpoints, /remember)<br/>writes ONLY to primaryProject"]
|
||||
```
|
||||
@@ -160,7 +160,7 @@ project names collide across workspaces. Reads route over the user's OAuth sessi
|
||||
| `hooks/session-start.sh`, `hooks/pre-compact.sh` | the ambient bridge (read / write) |
|
||||
| `hooks/hooks.json` | registers the hooks |
|
||||
| `output-styles/basic-memory.md` | the capture reflexes |
|
||||
| `skills/{setup,remember,share,status}/` | the deliberate slash commands |
|
||||
| `skills/{bm-setup,bm-remember,bm-share,bm-status}/` | the deliberate slash commands |
|
||||
| `schemas/{session,decision,task}.md` | picoschema seeds (copied into your project at setup) |
|
||||
| `.claude/settings.json` → `basicMemory` | per-project configuration |
|
||||
| your Basic Memory projects | all actual content |
|
||||
|
||||
@@ -37,7 +37,7 @@ SessionStart, PreCompact**.
|
||||
In a project (repo) where you want memory, run:
|
||||
|
||||
```
|
||||
/basic-memory:setup
|
||||
/basic-memory:bm-setup
|
||||
```
|
||||
|
||||
It's a short interview. It will:
|
||||
@@ -57,7 +57,7 @@ It's a short interview. It will:
|
||||
When it finishes, run:
|
||||
|
||||
```
|
||||
/basic-memory:status
|
||||
/basic-memory:bm-status
|
||||
```
|
||||
|
||||
to see exactly what the plugin is tracking.
|
||||
@@ -67,7 +67,7 @@ to see exactly what the plugin is tracking.
|
||||
1. **Capture a decision.** In normal conversation, make a decision — e.g. *"Let's use
|
||||
Postgres, not SQLite, because we need concurrent writers."* With the output style on,
|
||||
Claude writes a `type: decision` note and tells you the permalink.
|
||||
2. **Quick-capture something.** `/basic-memory:remember switch the staging job to the
|
||||
2. **Quick-capture something.** `/basic-memory:bm-remember switch the staging job to the
|
||||
new image after the rebase lands` → saved to `bm-remember/`.
|
||||
3. **Start a fresh session.** Open a new Claude Code session in the same project. The
|
||||
**SessionStart brief** appears first thing, showing your active tasks and the open
|
||||
@@ -83,7 +83,7 @@ graph accumulates.
|
||||
On Basic Memory Cloud with a team workspace, you can read team context into your brief
|
||||
and publish back deliberately.
|
||||
|
||||
Re-run `/basic-memory:setup` (or edit `.claude/settings.json`). Because project names
|
||||
Re-run `/basic-memory:bm-setup` (or edit `.claude/settings.json`). Because project names
|
||||
repeat across workspaces, team projects use **workspace-qualified names**
|
||||
(`my-team/notes`) or `external_id` UUIDs — setup finds these for you via
|
||||
`list_workspaces`.
|
||||
@@ -102,7 +102,7 @@ repeat across workspaces, team projects use **workspace-qualified names**
|
||||
Now:
|
||||
- SessionStart folds the team's **open decisions** into your brief (read-only).
|
||||
- Your captures still go **only** to `primaryProject` — never to the team.
|
||||
- `/basic-memory:share <note>` publishes a chosen note to `my-team/notes/shared`, with
|
||||
- `/basic-memory:bm-share <note>` publishes a chosen note to `my-team/notes/shared`, with
|
||||
attribution and a confirmation step.
|
||||
|
||||
Tip: a team brief is only as rich as the team's typed notes. Share an existing decision
|
||||
@@ -116,9 +116,9 @@ Everything is in the `basicMemory` block of `.claude/settings.json`. Common knob
|
||||
|-----|---------|--------------|
|
||||
| `primaryProject` | (default project) | where briefs read from and captures write to |
|
||||
| `secondaryProjects` | `[]` | team/shared projects read for recall (read-only) |
|
||||
| `teamProjects` | `{}` | share targets for `/basic-memory:share` |
|
||||
| `teamProjects` | `{}` | share targets for `/basic-memory:bm-share` |
|
||||
| `captureFolder` | `sessions` | folder for PreCompact checkpoints |
|
||||
| `rememberFolder` | `bm-remember` | folder for `/basic-memory:remember` |
|
||||
| `rememberFolder` | `bm-remember` | folder for `/basic-memory:bm-remember` |
|
||||
| `recallTimeframe` | `3d` | recency window for the brief |
|
||||
| `preCompactCapture` | `extractive` | how checkpoints are produced |
|
||||
|
||||
@@ -126,7 +126,7 @@ See [settings.example.json](../settings.example.json) for the full shape.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No brief at session start?** Confirm Basic Memory is connected (`/basic-memory:status`).
|
||||
- **No brief at session start?** Confirm Basic Memory is connected (`/basic-memory:bm-status`).
|
||||
The hooks are silent if `basic-memory` isn't on PATH.
|
||||
- **Checkpoints aren't being written?** A `primaryProject` must be set — the PreCompact
|
||||
hook never writes to an un-pinned/default project on its own.
|
||||
|
||||
@@ -72,18 +72,18 @@ often on a **team**.
|
||||
> team's recent open decisions. You're oriented before you ask a single question.
|
||||
|
||||
**Wins:** no context bleeding between projects; team memory that compounds across
|
||||
people; sharing a decision to the team in one gesture (`/basic-memory:share`).
|
||||
people; sharing a decision to the team in one gesture (`/basic-memory:bm-share`).
|
||||
|
||||
## What you actually get
|
||||
|
||||
Once installed and set up (`/basic-memory:setup`):
|
||||
Once installed and set up (`/basic-memory:bm-setup`):
|
||||
|
||||
- **Session briefings** — start each session knowing your active tasks, open decisions,
|
||||
and (if on a team) recent team context.
|
||||
- **Checkpoints that survive compaction** — long sessions don't lose their thread.
|
||||
- **Capture reflexes** — Claude searches before answering recall questions and writes
|
||||
down real decisions as it goes, citing permalinks.
|
||||
- **Quick capture** — `/basic-memory:remember` for a fast note without breaking flow.
|
||||
- **Quick capture** — `/basic-memory:bm-remember` for a fast note without breaking flow.
|
||||
- **Team memory** — read across shared projects; publish back deliberately.
|
||||
|
||||
All of it in plain Markdown files you own, in projects you control — local, cloud, or
|
||||
|
||||
@@ -177,7 +177,7 @@ with ThreadPoolExecutor(max_workers=3 + MAX_SHARED) as pool:
|
||||
# The first-run nudge — shown until setup writes a basicMemory config block.
|
||||
setup_nudge = (
|
||||
"_Basic Memory isn't set up for this project yet. Run "
|
||||
"`/basic-memory:setup` (~2 min) to configure session briefings and checkpoints._"
|
||||
"`/basic-memory:bm-setup` (~2 min) to configure session briefings and checkpoints._"
|
||||
)
|
||||
|
||||
# Trigger: every primary query failed (no default project, misnamed project,
|
||||
@@ -193,7 +193,7 @@ if primary_tasks is None and primary_decisions is None and primary_sessions is N
|
||||
print(
|
||||
"# Basic Memory\n\n"
|
||||
f"_Couldn't read from `{proj}` — it may be misnamed or unreachable. "
|
||||
"Run `/basic-memory:status` to check._"
|
||||
"Run `/basic-memory:bm-status` to check._"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -246,7 +246,7 @@ if shared_sections:
|
||||
lines += [
|
||||
"",
|
||||
"_Shared-project context is read-only. Your captures stay in this project; "
|
||||
"use `/basic-memory:share` to deliberately promote a note to the team._",
|
||||
"use `/basic-memory:bm-share` to deliberately promote a note to the team._",
|
||||
]
|
||||
if shared_capped:
|
||||
lines += ["", f"_(reading the first {MAX_SHARED} shared projects; more are configured.)_"]
|
||||
|
||||
@@ -24,7 +24,7 @@ settings:
|
||||
A **DecisionNote** is a durable record of a real choice — one with alternatives
|
||||
and a rationale, not a passing preference. The Basic Memory plugin's output-style
|
||||
prompts Claude to capture these inline as decisions are made, and the future
|
||||
`/basic-memory:decide` command captures them explicitly.
|
||||
`/basic-memory:bm-decide` command captures them explicitly.
|
||||
|
||||
Decisions are found by structured recall:
|
||||
`search_notes(metadata_filters={"type": "decision", "status": "open"})`.
|
||||
|
||||
@@ -25,7 +25,7 @@ settings:
|
||||
# Session
|
||||
|
||||
A **SessionNote** is a resume checkpoint written by the Basic Memory plugin's
|
||||
PreCompact hook (and, later, the `/basic-memory:handoff` command) right before
|
||||
PreCompact hook (and, later, the `/basic-memory:bm-handoff` command) right before
|
||||
Claude Code compacts the context window. It records what the session was doing
|
||||
so the next session can pick up where this one left off.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$comment": "Example Basic Memory plugin settings. Copy the basicMemory block (and optionally outputStyle) into your project's .claude/settings.json, then set primaryProject. The easiest way to fill this in is /basic-memory:setup. Team projects (secondaryProjects, teamProjects) must use workspace-qualified names like 'my-team/notes' or external_id UUIDs — bare names are ambiguous across workspaces. See DESIGN.md for the full schema.",
|
||||
"$comment": "Example Basic Memory plugin settings. Copy the basicMemory block (and optionally outputStyle) into your project's .claude/settings.json, then set primaryProject. The easiest way to fill this in is /basic-memory:bm-setup. Team projects (secondaryProjects, teamProjects) must use workspace-qualified names like 'my-team/notes' or external_id UUIDs — bare names are ambiguous across workspaces. See DESIGN.md for the full schema.",
|
||||
"basicMemory": {
|
||||
"primaryProject": "my-project",
|
||||
"secondaryProjects": ["my-team/main", "my-team/notes"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: remember
|
||||
description: Quickly capture a thought, fact, or reminder into Basic Memory as a lightweight note. Use when the user says "remember that…", "note this", "save this to memory", or runs /basic-memory:remember. For quick deliberate capture — not full decision or session records.
|
||||
name: bm-remember
|
||||
description: Quickly capture a thought, fact, or reminder into Basic Memory as a lightweight note. Use when the user says "remember that…", "note this", "save this to memory", or runs /basic-memory:bm-remember. For quick deliberate capture — not full decision or session records.
|
||||
argument-hint: <text to remember>
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: setup
|
||||
description: Set up the Basic Memory plugin for this project — a short guided interview that configures the project mapping, seeds note schemas, learns or suggests placement conventions, and enables capture reflexes. Use when the user runs /basic-memory:setup, says "set up basic memory", or asks to configure/bootstrap the plugin.
|
||||
name: bm-setup
|
||||
description: Set up the Basic Memory plugin for this project — a short guided interview that configures the project mapping, seeds note schemas, learns or suggests placement conventions, and enables capture reflexes. Use when the user runs /basic-memory:bm-setup, says "set up basic memory", or asks to configure/bootstrap the plugin.
|
||||
argument-hint: (no arguments — runs an interactive interview)
|
||||
---
|
||||
|
||||
@@ -71,7 +71,7 @@ Ask only what you can't infer. Cover:
|
||||
six, order the most relevant first and tell them the rest are configured but
|
||||
not read each session.
|
||||
- **Share target** (optional): if the user wants a place to *publish* notes to the
|
||||
team via `/basic-memory:share`, add it to `teamProjects` as
|
||||
team via `/basic-memory:bm-share`, add it to `teamProjects` as
|
||||
`"<qualified-name>": { "promoteFolder": "shared" }`. Sharing is always a manual
|
||||
gesture — auto-capture never writes to a team project.
|
||||
|
||||
@@ -102,7 +102,7 @@ Ask only what you can't infer. Cover:
|
||||
6. **How active should I be? (output style)** "Want me to proactively capture —
|
||||
search the graph before recalling, write material decisions as typed notes, and
|
||||
cite permalinks? Or keep it quiet (just the session brief, the PreCompact
|
||||
checkpoint, and `/basic-memory:remember` on demand)?" Enabling it sets
|
||||
checkpoint, and `/basic-memory:bm-remember` on demand)?" Enabling it sets
|
||||
`outputStyle: "basic-memory"`. Default to enabled; leave it off for a recall-only,
|
||||
low-noise setup. (This is the single knob for how proactive the assistant is —
|
||||
the hooks always run regardless.)
|
||||
@@ -119,7 +119,7 @@ Ask only what you can't infer. Cover:
|
||||
### 1. Seed the schemas
|
||||
The plugin ships seed schemas at `<plugin>/schemas/` — that's **two directories up
|
||||
from this skill's directory, then `schemas/`** (this skill is at
|
||||
`<plugin>/skills/setup/`). Read `session.md`, `decision.md`, and `task.md` there.
|
||||
`<plugin>/skills/bm-setup/`). Read `session.md`, `decision.md`, and `task.md` there.
|
||||
|
||||
For each one:
|
||||
- Check whether the chosen project already has a schema for that type
|
||||
@@ -156,7 +156,7 @@ git ls-files skills/ | grep -q memory- && echo "source repo - skip install"
|
||||
Otherwise, run from the project root:
|
||||
|
||||
```
|
||||
npx skills add basicmachines-co/basic-memory --path skills
|
||||
npx skills add basicmachines-co/basic-memory/skills
|
||||
```
|
||||
|
||||
This installs the canonical `memory-*` skills into the user's skills directory — the
|
||||
@@ -222,5 +222,5 @@ Then handle activation based on the output style:
|
||||
proactive-capture reflexes wait for the restart.
|
||||
- **Output style off** → no restart needed; the hooks already run.
|
||||
|
||||
End with: *"Done — I'll use this from the next message. Run `/basic-memory:status`
|
||||
End with: *"Done — I'll use this from the next message. Run `/basic-memory:bm-status`
|
||||
anytime to see what I'm tracking."*
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: share
|
||||
description: Promote a note from your personal Basic Memory project to a shared team project, with attribution. Use when the user says "share this with the team", "publish this decision", or runs /basic-memory:share. This is the deliberate way to write to a team workspace — auto-capture never does.
|
||||
name: bm-share
|
||||
description: Promote a note from your personal Basic Memory project to a shared team project, with attribution. Use when the user says "share this with the team", "publish this decision", or runs /basic-memory:bm-share. This is the deliberate way to write to a team workspace — auto-capture never does.
|
||||
argument-hint: <note title or permalink to share>
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ argument-hint: <note title or permalink to share>
|
||||
|
||||
Copy a note from the personal/primary project into a configured **team project** so
|
||||
teammates can see it. This is the *only* path by which the plugin writes to a shared
|
||||
project — session checkpoints and `/basic-memory:remember` always stay personal.
|
||||
project — session checkpoints and `/basic-memory:bm-remember` always stay personal.
|
||||
|
||||
## Steps
|
||||
|
||||
@@ -19,7 +19,7 @@ project — session checkpoints and `/basic-memory:remember` always stay persona
|
||||
- `primaryProject` — the source project notes are read from.
|
||||
|
||||
If `teamProjects` is empty, tell the user there's no share target configured and
|
||||
suggest adding one (or running `/basic-memory:setup`), then stop. Don't invent a
|
||||
suggest adding one (or running `/basic-memory:bm-setup`), then stop. Don't invent a
|
||||
target.
|
||||
|
||||
2. **Find the source note.** From `$ARGUMENTS` (a title, permalink, or `memory://`
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: status
|
||||
name: bm-status
|
||||
description: Show the Basic Memory plugin's current state for this project — active project, capture folders, output style, recent session checkpoints, and whether Basic Memory is reachable.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
@@ -19,7 +19,7 @@ This is a quick diagnostic — gather the facts and lay them out; don't over-inv
|
||||
if present) and report:
|
||||
- From the `basicMemory` block: `primaryProject` (or note none is pinned — the
|
||||
default project is used), `secondaryProjects` (team/shared read sources),
|
||||
`teamProjects` (share targets for `/basic-memory:share`), `captureFolder`
|
||||
`teamProjects` (share targets for `/basic-memory:bm-share`), `captureFolder`
|
||||
(default `sessions`), `rememberFolder` (default `bm-remember`), and
|
||||
`preCompactCapture` mode (default `extractive`).
|
||||
- From the **root** settings object (not `basicMemory`): whether `outputStyle` is
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "codex",
|
||||
"version": "0.21.6",
|
||||
"description": "A Codex-native bridge to Basic Memory for durable engineering context, decisions, and resumable checkpoints.",
|
||||
"author": {
|
||||
"name": "Basic Machines",
|
||||
"email": "hello@basicmachines.co",
|
||||
"url": "https://basicmemory.com"
|
||||
},
|
||||
"homepage": "https://docs.basicmemory.com",
|
||||
"repository": "https://github.com/basicmachines-co/basic-memory/tree/main/plugins/codex",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"basic-memory",
|
||||
"codex",
|
||||
"memory",
|
||||
"knowledge-graph",
|
||||
"mcp",
|
||||
"checkpoints"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Basic Memory for Codex",
|
||||
"shortDescription": "Carry decisions, active work, and handoffs across Codex threads",
|
||||
"longDescription": "Use Basic Memory for Codex to orient from your durable knowledge graph, capture engineering decisions, checkpoint long-running work, and resume with repo-backed context across Codex sessions.",
|
||||
"developerName": "Basic Machines",
|
||||
"category": "Developer Tools",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://basicmemory.com",
|
||||
"privacyPolicyURL": "https://basicmemory.com/privacy",
|
||||
"termsOfServiceURL": "https://basicmemory.com/terms",
|
||||
"defaultPrompt": [
|
||||
"Use Basic Memory to orient before changing this repo.",
|
||||
"Checkpoint this Codex thread into Basic Memory.",
|
||||
"Capture the decision we just made."
|
||||
],
|
||||
"brandColor": "#2563EB",
|
||||
"composerIcon": "./assets/app-icon.png",
|
||||
"logo": "./assets/logo.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Basic Memory Codex Plugin Development
|
||||
|
||||
This plugin is developed in-place from the Basic Memory repository. Codex installs local plugins
|
||||
through marketplaces, so local testing uses a repo-local marketplace wrapper rather than publishing
|
||||
anything external.
|
||||
|
||||
## Local Marketplace
|
||||
|
||||
The repo marketplace lives at:
|
||||
|
||||
```text
|
||||
.agents/plugins/marketplace.json
|
||||
```
|
||||
|
||||
It exposes this plugin as:
|
||||
|
||||
```text
|
||||
codex@basic-memory-local
|
||||
```
|
||||
|
||||
The marketplace entry points at `./plugins/codex`, resolved relative to the repository root.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add "$(git rev-parse --show-toplevel)"
|
||||
codex plugin add codex@basic-memory-local
|
||||
```
|
||||
|
||||
Start a new Codex thread after installing. New threads are the reliable boundary for picking up
|
||||
plugin skills, hooks, and MCP configuration.
|
||||
|
||||
Plugin installation is user-level in Codex, so one install makes the plugin available across
|
||||
projects on the same machine. Repo-specific memory routing still comes from each checkout's
|
||||
`.codex/basic-memory.json`.
|
||||
|
||||
## Iteration Loop
|
||||
|
||||
After changing files in `plugins/codex`, run the local checks:
|
||||
|
||||
```bash
|
||||
just package-check-codex
|
||||
```
|
||||
|
||||
Then update the manifest cachebuster and reinstall from the local marketplace:
|
||||
|
||||
```bash
|
||||
python3 "$CODEX_PLUGIN_CREATOR_SCRIPTS/update_plugin_cachebuster.py" \
|
||||
"$(git rev-parse --show-toplevel)/plugins/codex"
|
||||
codex plugin add codex@basic-memory-local
|
||||
```
|
||||
|
||||
Start a fresh Codex thread to test the updated plugin.
|
||||
|
||||
## Useful Checks
|
||||
|
||||
List configured marketplaces:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace list
|
||||
```
|
||||
|
||||
List plugins Codex can see:
|
||||
|
||||
```bash
|
||||
codex plugin list
|
||||
```
|
||||
|
||||
Run the full package validation gate when touching plugin packaging, shared skills, or integration
|
||||
metadata:
|
||||
|
||||
```bash
|
||||
just package-check
|
||||
```
|
||||
|
||||
To also run Codex's scaffold validator during `just package-check-codex`, set
|
||||
`CODEX_PLUGIN_VALIDATOR` to the local `plugin-creator` validator script before
|
||||
running the check.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Basic Memory for Codex
|
||||
|
||||
Basic Memory for Codex is the Codex-native bridge between a working coding thread
|
||||
and Basic Memory's durable knowledge graph.
|
||||
|
||||
It is not a 1:1 copy of the Claude Code plugin. This version leans into Codex
|
||||
workflows: repo orientation, long-running goals, changed-file evidence, explicit
|
||||
verification, decision capture, and resumable checkpoints.
|
||||
|
||||
## What It Does
|
||||
|
||||
- **Orient from memory.** The `bm-orient` skill reads active tasks, open
|
||||
decisions, and recent Codex checkpoints before substantial work.
|
||||
- **Checkpoint work.** The `bm-checkpoint` skill and `PreCompact` hook write
|
||||
`type: codex_session` notes with the current work cursor.
|
||||
- **Capture decisions.** The `bm-decide` skill records durable engineering
|
||||
decisions with rationale, alternatives, and consequences.
|
||||
- **Remember lightly.** The `bm-remember` skill saves small facts without turning
|
||||
them into a full decision or session note.
|
||||
- **Share deliberately.** The `bm-share` skill copies personal notes to configured
|
||||
team projects only after confirmation.
|
||||
- **Report status.** The `bm-status` skill shows configuration, reachability, and
|
||||
recent memory state.
|
||||
|
||||
## Package Contents
|
||||
|
||||
| Path | Role |
|
||||
| --- | --- |
|
||||
| `.codex-plugin/plugin.json` | Codex plugin manifest |
|
||||
| `.mcp.json` | Basic Memory MCP server configuration |
|
||||
| `hooks/hooks.json` | SessionStart and PreCompact hook registration |
|
||||
| `hooks/session-start.sh` | Launches the SessionStart uv script |
|
||||
| `hooks/session-start.py` | Injects a compact memory brief at thread start |
|
||||
| `hooks/pre-compact.sh` | Launches the PreCompact uv script |
|
||||
| `hooks/pre-compact.py` | Writes an automatic Codex checkpoint before compaction |
|
||||
| `skills/` | Codex-native Basic Memory workflows |
|
||||
| `schemas/` | Seed schemas for Codex sessions, decisions, and tasks |
|
||||
|
||||
## Install
|
||||
|
||||
Install the plugin once from the Basic Memory repository root:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add "$(git rev-parse --show-toplevel)"
|
||||
codex plugin add codex@basic-memory-local
|
||||
```
|
||||
|
||||
Plugin installation is user-level in Codex, so one install makes the plugin
|
||||
available across projects on the same machine. Start a new Codex thread after
|
||||
installing so Codex can load the plugin skills, MCP configuration, and hooks.
|
||||
|
||||
Each repository still needs its own `.codex/basic-memory.json` so the plugin
|
||||
knows which Basic Memory project and folders to use for that checkout. Run the
|
||||
setup skill in each repo, or create the config file shown below.
|
||||
|
||||
## Configuration
|
||||
|
||||
Run the setup skill, or create `.codex/basic-memory.json` in a repo:
|
||||
|
||||
```json
|
||||
{
|
||||
"basicMemory": {
|
||||
"primaryProject": "my-project",
|
||||
"secondaryProjects": [],
|
||||
"teamProjects": {},
|
||||
"focus": "code/dev",
|
||||
"captureFolder": "codex-sessions",
|
||||
"rememberFolder": "codex-remember",
|
||||
"recallTimeframe": "7d",
|
||||
"placementConventions": "Put decisions in decisions/ and work checkpoints in codex-sessions/."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Codex plugin hooks must be reviewed and trusted before they run. Open `/hooks` in
|
||||
Codex after enabling the plugin and trust the Basic Memory hook definitions.
|
||||
|
||||
## Development
|
||||
|
||||
From this directory:
|
||||
|
||||
```bash
|
||||
just check
|
||||
```
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
just package-check-codex
|
||||
```
|
||||
|
||||
The package intentionally keeps Codex-specific configuration separate from
|
||||
Claude's `.claude/settings.json`.
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"description": "Basic Memory for Codex hooks - orient from the graph on session start and checkpoint before compaction.",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${PLUGIN_ROOT}/hooks/session-start.sh",
|
||||
"statusMessage": "Loading Basic Memory context",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "manual|auto",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${PLUGIN_ROOT}/hooks/pre-compact.sh",
|
||||
"statusMessage": "Checkpointing Codex work to Basic Memory",
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
"""Checkpoint Codex work into Basic Memory before compaction."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
UUID_RE = re.compile(
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def basic_memory_command() -> list[str] | None:
|
||||
configured = os.environ.get("BM_BIN")
|
||||
if configured:
|
||||
return shlex.split(configured)
|
||||
if shutil.which("basic-memory"):
|
||||
return ["basic-memory"]
|
||||
if shutil.which("bm"):
|
||||
return ["bm"]
|
||||
if shutil.which("uvx"):
|
||||
return ["uvx", "basic-memory"]
|
||||
if shutil.which("uv"):
|
||||
return ["uv", "tool", "run", "basic-memory"]
|
||||
return None
|
||||
|
||||
|
||||
def parse_payload() -> dict:
|
||||
try:
|
||||
payload = json.loads(sys.stdin.read() or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def load_config(directory: Path) -> dict:
|
||||
path = directory / ".codex" / "basic-memory.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return data.get("basicMemory", data)
|
||||
|
||||
|
||||
def text_of(content):
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text = block.get("text")
|
||||
if isinstance(text, str):
|
||||
parts.append(text)
|
||||
return "\n".join(parts)
|
||||
return ""
|
||||
|
||||
|
||||
def transcript_turns(path: str):
|
||||
collected = []
|
||||
if not path:
|
||||
return collected
|
||||
try:
|
||||
with open(path) as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if obj.get("isMeta") or obj.get("toolUseResult") is not None:
|
||||
continue
|
||||
msg = obj.get("message") if isinstance(obj.get("message"), dict) else obj
|
||||
role = msg.get("role") or obj.get("type")
|
||||
if role not in ("user", "assistant"):
|
||||
continue
|
||||
text = text_of(msg.get("content")).strip()
|
||||
if text:
|
||||
collected.append((role, text))
|
||||
except Exception:
|
||||
return []
|
||||
return collected
|
||||
|
||||
|
||||
def git_status(directory: Path) -> list[str]:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "status", "--short"],
|
||||
cwd=directory,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
if out.returncode != 0:
|
||||
return []
|
||||
return [line for line in out.stdout.splitlines() if line.strip()][:20]
|
||||
|
||||
|
||||
def clip(value: str, limit: int) -> str:
|
||||
compact = " ".join(value.split())
|
||||
return compact if len(compact) <= limit else compact[: limit - 1].rstrip() + "..."
|
||||
|
||||
|
||||
def main() -> int:
|
||||
bm_cmd = basic_memory_command()
|
||||
if not bm_cmd:
|
||||
return 0
|
||||
|
||||
payload = parse_payload()
|
||||
cwd = Path(payload.get("cwd") or os.getcwd())
|
||||
transcript_path = payload.get("transcript_path") or ""
|
||||
session_id = payload.get("session_id") or ""
|
||||
turn_id = payload.get("turn_id") or ""
|
||||
trigger = payload.get("trigger") or ""
|
||||
model = payload.get("model") or ""
|
||||
|
||||
cfg = load_config(cwd)
|
||||
primary_project = str(cfg.get("primaryProject") or "").strip()
|
||||
capture_folder = str(cfg.get("captureFolder") or "codex-sessions").strip()
|
||||
|
||||
if not primary_project:
|
||||
return 0
|
||||
|
||||
conversation = transcript_turns(transcript_path)
|
||||
if not conversation or not any(role == "user" for role, _ in conversation):
|
||||
return 0
|
||||
|
||||
user_messages = [text for role, text in conversation if role == "user"]
|
||||
assistant_messages = [text for role, text in conversation if role == "assistant"]
|
||||
opening = user_messages[0] if user_messages else ""
|
||||
recent_user = user_messages[-3:]
|
||||
recent_assistant = assistant_messages[-2:]
|
||||
status_lines = git_status(cwd)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
iso = now.isoformat(timespec="seconds")
|
||||
title = f"Codex session {now.strftime('%Y-%m-%d %H:%M:%S')} - {clip(opening, 40)}"
|
||||
|
||||
frontmatter = [
|
||||
"---",
|
||||
"type: codex_session",
|
||||
"status: open",
|
||||
f"started: {iso}",
|
||||
f"ended: {iso}",
|
||||
f"project: {primary_project}",
|
||||
f"cwd: {cwd}",
|
||||
]
|
||||
if session_id:
|
||||
frontmatter.append(f"codex_session_id: {session_id}")
|
||||
if turn_id:
|
||||
frontmatter.append(f"codex_turn_id: {turn_id}")
|
||||
if trigger:
|
||||
frontmatter.append(f"trigger: {trigger}")
|
||||
if model:
|
||||
frontmatter.append(f"model: {model}")
|
||||
frontmatter += ["capture: extractive", "---"]
|
||||
|
||||
body = [
|
||||
"",
|
||||
f"# {title}",
|
||||
"",
|
||||
"_Automatic Codex pre-compaction checkpoint. It records the working cursor, "
|
||||
"not a polished summary._",
|
||||
"",
|
||||
"## Summary",
|
||||
f"Working in `{cwd}`.",
|
||||
f"- Opening request: {clip(opening, 300)}" if opening else "",
|
||||
"",
|
||||
"## Recent User Cursor",
|
||||
]
|
||||
body += [f"- {clip(message, 240)}" for message in recent_user]
|
||||
if recent_assistant:
|
||||
body += ["", "## Recent Assistant Notes"]
|
||||
body += [f"- {clip(message, 240)}" for message in recent_assistant]
|
||||
if status_lines:
|
||||
body += ["", "## Working Tree"]
|
||||
body += [f"- `{line}`" for line in status_lines]
|
||||
body += [
|
||||
"",
|
||||
"## Observations",
|
||||
f"- [context] Codex worked in `{cwd}`",
|
||||
f"- [context] Session opened with: {clip(opening, 200)}" if opening else "",
|
||||
"- [next_step] Re-read this checkpoint, inspect the current worktree, and "
|
||||
"continue from the latest user request",
|
||||
]
|
||||
|
||||
content = "\n".join(frontmatter + body)
|
||||
project_flag = "--project-id" if UUID_RE.match(primary_project) else "--project"
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
*bm_cmd,
|
||||
"tool",
|
||||
"write-note",
|
||||
"--title",
|
||||
title,
|
||||
"--folder",
|
||||
capture_folder,
|
||||
project_flag,
|
||||
primary_project,
|
||||
"--tags",
|
||||
"codex",
|
||||
"--tags",
|
||||
"auto-capture",
|
||||
],
|
||||
input=content,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except Exception:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# PreCompact hook - checkpoint Codex work into Basic Memory before compaction.
|
||||
#
|
||||
# Contract: best effort. The hook only writes when .codex/basic-memory.json pins a
|
||||
# primary project, and every failure exits 0 so compaction can continue.
|
||||
|
||||
set -u
|
||||
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
uv run --script "$script_dir/pre-compact.py" 2>/dev/null || exit 0
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
"""Brief Codex from Basic Memory at thread start."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
UUID_RE = re.compile(
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
MAX_SHARED = 6
|
||||
|
||||
|
||||
def basic_memory_command() -> list[str] | None:
|
||||
configured = os.environ.get("BM_BIN")
|
||||
if configured:
|
||||
return shlex.split(configured)
|
||||
if shutil.which("basic-memory"):
|
||||
return ["basic-memory"]
|
||||
if shutil.which("bm"):
|
||||
return ["bm"]
|
||||
if shutil.which("uvx"):
|
||||
return ["uvx", "basic-memory"]
|
||||
if shutil.which("uv"):
|
||||
return ["uv", "tool", "run", "basic-memory"]
|
||||
return None
|
||||
|
||||
|
||||
def parse_payload() -> dict:
|
||||
try:
|
||||
payload = json.loads(sys.stdin.read() or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def load_config(directory: Path) -> tuple[dict, bool]:
|
||||
path = directory / ".codex" / "basic-memory.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
return {}, False
|
||||
except Exception:
|
||||
return {}, True
|
||||
if not isinstance(data, dict):
|
||||
return {}, True
|
||||
return data.get("basicMemory", data), True
|
||||
|
||||
|
||||
def project_args(project_ref: str | None) -> list[str]:
|
||||
if not project_ref:
|
||||
return []
|
||||
flag = "--project-id" if UUID_RE.match(project_ref) else "--project"
|
||||
return [flag, project_ref]
|
||||
|
||||
|
||||
def search(
|
||||
bm_cmd: list[str],
|
||||
filters: list[str],
|
||||
project_ref: str | None = None,
|
||||
timeout: int = 10,
|
||||
):
|
||||
cmd = [*bm_cmd, "tool", "search-notes", *filters, "--page-size", "5"]
|
||||
cmd.extend(project_args(project_ref))
|
||||
try:
|
||||
out = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
return json.loads(out.stdout)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def rows(result):
|
||||
return (result or {}).get("results") or []
|
||||
|
||||
|
||||
def label(result):
|
||||
name = result.get("title") or result.get("file_path") or "(untitled)"
|
||||
ref = result.get("permalink") or result.get("file_path") or ""
|
||||
return f"- {name}" + (f" - {ref}" if ref else "")
|
||||
|
||||
|
||||
def readable(ref):
|
||||
return f"{ref[:8]}..." if UUID_RE.match(ref) else ref
|
||||
|
||||
|
||||
def shared_project_refs(cfg: dict, primary_project: str) -> tuple[list[str], bool]:
|
||||
secondary = cfg.get("secondaryProjects")
|
||||
secondary = secondary if isinstance(secondary, list) else []
|
||||
team = cfg.get("teamProjects")
|
||||
team = team if isinstance(team, dict) else {}
|
||||
|
||||
shared_refs: list[str] = []
|
||||
for ref in list(secondary) + list(team.keys()):
|
||||
if isinstance(ref, str) and ref.strip() and ref.strip() != primary_project:
|
||||
clean = ref.strip()
|
||||
if clean not in shared_refs:
|
||||
shared_refs.append(clean)
|
||||
shared_capped = len(shared_refs) > MAX_SHARED
|
||||
return shared_refs[:MAX_SHARED], shared_capped
|
||||
|
||||
|
||||
def no_context_message(configured: bool, primary_project: str) -> str:
|
||||
if not configured:
|
||||
return (
|
||||
"# Basic Memory for Codex\n\n"
|
||||
"_This repo is not configured for Basic Memory yet. Run `Use Basic Memory "
|
||||
"for Codex to set up this repo` to map a project, seed schemas, and turn "
|
||||
"on Codex checkpoints._"
|
||||
)
|
||||
|
||||
project = primary_project or "the default project"
|
||||
return (
|
||||
"# Basic Memory for Codex\n\n"
|
||||
f"_Could not read from `{project}`. Run `Use bm-status` to check the "
|
||||
"Basic Memory project mapping._"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
bm_cmd = basic_memory_command()
|
||||
if not bm_cmd:
|
||||
return 0
|
||||
|
||||
payload = parse_payload()
|
||||
cwd = Path(payload.get("cwd") or os.getcwd())
|
||||
source = payload.get("source") or "startup"
|
||||
|
||||
cfg, configured = load_config(cwd)
|
||||
primary_project = str(cfg.get("primaryProject") or "").strip()
|
||||
recall_timeframe = str(cfg.get("recallTimeframe") or "7d").strip()
|
||||
capture_folder = str(cfg.get("captureFolder") or "codex-sessions").strip()
|
||||
placement = str(cfg.get("placementConventions") or "").strip()
|
||||
focus = str(cfg.get("focus") or "").strip()
|
||||
shared_refs, shared_capped = shared_project_refs(cfg, primary_project)
|
||||
|
||||
active_tasks = ["--type", "task", "--status", "active"]
|
||||
open_decisions = ["--type", "decision", "--status", "open"]
|
||||
recent_codex = ["--type", "codex_session", "--after_date", recall_timeframe]
|
||||
recent_generic = ["--type", "session", "--after_date", recall_timeframe]
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4 + MAX_SHARED) as pool:
|
||||
fut_tasks = pool.submit(search, bm_cmd, active_tasks, primary_project or None)
|
||||
fut_decisions = pool.submit(search, bm_cmd, open_decisions, primary_project or None)
|
||||
fut_codex = pool.submit(search, bm_cmd, recent_codex, primary_project or None)
|
||||
fut_sessions = pool.submit(search, bm_cmd, recent_generic, primary_project or None)
|
||||
fut_shared = {ref: pool.submit(search, bm_cmd, open_decisions, ref) for ref in shared_refs}
|
||||
primary_tasks = fut_tasks.result()
|
||||
primary_decisions = fut_decisions.result()
|
||||
primary_codex = fut_codex.result()
|
||||
primary_sessions = fut_sessions.result()
|
||||
shared_results = {ref: fut.result() for ref, fut in fut_shared.items()}
|
||||
|
||||
if primary_tasks is None and primary_decisions is None and primary_codex is None:
|
||||
print(no_context_message(configured, primary_project))
|
||||
return 0
|
||||
|
||||
lines = ["# Basic Memory for Codex", ""]
|
||||
header = f"Project: {primary_project or 'default project'}"
|
||||
if focus:
|
||||
header += f" | focus: {focus}"
|
||||
if shared_refs:
|
||||
header += f" | reading {len(shared_refs)} shared project(s)"
|
||||
lines.append(header)
|
||||
lines.append(f"Session source: {source}")
|
||||
|
||||
task_rows = rows(primary_tasks)
|
||||
decision_rows = rows(primary_decisions)
|
||||
codex_rows = rows(primary_codex)
|
||||
session_rows = rows(primary_sessions)
|
||||
|
||||
if task_rows:
|
||||
lines += ["", f"## Active Tasks ({len(task_rows)})", *[label(r) for r in task_rows]]
|
||||
if decision_rows:
|
||||
lines += [
|
||||
"",
|
||||
f"## Open Decisions ({len(decision_rows)})",
|
||||
*[label(r) for r in decision_rows],
|
||||
]
|
||||
if codex_rows:
|
||||
lines += [
|
||||
"",
|
||||
f"## Recent Codex Checkpoints ({len(codex_rows)})",
|
||||
*[label(r) for r in codex_rows],
|
||||
]
|
||||
elif session_rows:
|
||||
lines += [
|
||||
"",
|
||||
f"## Recent Sessions ({len(session_rows)})",
|
||||
*[label(r) for r in session_rows],
|
||||
]
|
||||
|
||||
shared_sections = [(ref, rows(shared_results.get(ref))) for ref in shared_refs]
|
||||
shared_sections = [(ref, items) for ref, items in shared_sections if items]
|
||||
if shared_sections:
|
||||
lines += ["", "## Shared Context (Read Only)"]
|
||||
for ref, items in shared_sections:
|
||||
lines += [f"### {readable(ref)} open decisions", *[label(r) for r in items]]
|
||||
if shared_capped:
|
||||
lines += ["", f"Only the first {MAX_SHARED} shared projects are read on session start."]
|
||||
|
||||
if not (task_rows or decision_rows or codex_rows or session_rows or shared_sections):
|
||||
lines += ["", "_No active tasks, open decisions, or recent checkpoints found._"]
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"## Codex Memory Posture",
|
||||
"- Search Basic Memory before answering questions about prior decisions or status.",
|
||||
"- Capture durable engineering decisions as typed decision notes.",
|
||||
f"- Put automatic Codex checkpoints in `{capture_folder}/`.",
|
||||
]
|
||||
if placement:
|
||||
lines.append(f"- Follow these placement conventions for other notes: {placement}")
|
||||
else:
|
||||
lines.append("- Place other notes by topic, not in the checkpoint folder.")
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"Use Basic Memory as durable context, but keep required repo rules in AGENTS.md "
|
||||
"or checked-in docs.",
|
||||
]
|
||||
|
||||
print("\n".join(lines))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# SessionStart hook - brief Codex from Basic Memory at thread start.
|
||||
#
|
||||
# Contract: best effort only. A missing Basic Memory install, empty project, slow
|
||||
# cloud read, or bad config must never disrupt a Codex thread.
|
||||
|
||||
set -u
|
||||
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
uv run --script "$script_dir/session-start.py" 2>/dev/null || exit 0
|
||||
@@ -0,0 +1,19 @@
|
||||
# Basic Memory Codex plugin checks
|
||||
|
||||
repo_root := "../.."
|
||||
|
||||
# Validate the plugin manifest, hooks, skills, schemas, and MCP config.
|
||||
manifest-check:
|
||||
python3 {{repo_root}}/scripts/validate_codex_plugin.py .
|
||||
|
||||
# Validate against the local Codex plugin scaffold contract.
|
||||
scaffold-check:
|
||||
@validator="${CODEX_PLUGIN_VALIDATOR:-}"; \
|
||||
if [ -n "$validator" ]; then \
|
||||
cd {{repo_root}} && uv run python "$validator" plugins/codex; \
|
||||
else \
|
||||
echo "Skipping optional Codex scaffold validator: set CODEX_PLUGIN_VALIDATOR to enable"; \
|
||||
fi
|
||||
|
||||
# Run every local package check for this plugin.
|
||||
check: manifest-check scaffold-check
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Codex Session
|
||||
type: schema
|
||||
entity: CodexSession
|
||||
version: 1
|
||||
schema:
|
||||
summary?: string, one-paragraph what happened in this Codex thread
|
||||
changed_file?(array): string, files created, edited, deleted, or inspected
|
||||
verification?(array): string, checks run and their result
|
||||
decision?(array): string, decisions surfaced or created during the thread
|
||||
blocker?(array): string, unresolved blockers or failed approaches
|
||||
next_step?(array): string, explicit cursor for the next Codex thread
|
||||
produced?(array): Entity, notes or artifacts created or updated
|
||||
settings:
|
||||
validation: warn
|
||||
frontmatter:
|
||||
project: string, the Basic Memory project this session belongs to
|
||||
started: string, when the session began or checkpoint was created
|
||||
ended?: string, when the session was checkpointed
|
||||
status?(enum, lifecycle of the checkpoint): [open, resumed, closed]
|
||||
cwd?: string, working directory for the Codex thread
|
||||
codex_session_id?: string, Codex session identifier
|
||||
codex_turn_id?: string, Codex turn identifier
|
||||
trigger?: string, compaction trigger or deliberate checkpoint source
|
||||
model?: string, active Codex model slug when known
|
||||
capture?(enum, how this checkpoint was produced): [extractive, deliberate, summarized]
|
||||
---
|
||||
|
||||
# Codex Session
|
||||
|
||||
A **CodexSession** note is a resumable engineering checkpoint. It captures the
|
||||
thread cursor: what changed, what was verified, what decisions matter, and what
|
||||
the next Codex thread should do first.
|
||||
|
||||
Codex sessions are found by structured recall:
|
||||
`search_notes(metadata_filters={"type": "codex_session"}, after_date="7d")`.
|
||||
|
||||
## What Goes In A CodexSession
|
||||
|
||||
- **summary** - what happened.
|
||||
- **changed_file** - changed or inspected paths that matter to resume.
|
||||
- **verification** - commands actually run and their outcome.
|
||||
- **decision** - choices made or surfaced.
|
||||
- **blocker** - open failures, constraints, or rejected approaches.
|
||||
- **next_step** - the next concrete action.
|
||||
|
||||
Validation is `warn` so checkpointing never blocks the user's flow.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Decision
|
||||
type: schema
|
||||
entity: Decision
|
||||
version: 1
|
||||
schema:
|
||||
decision: string, the choice that was made
|
||||
rationale?: string, why this choice over alternatives
|
||||
alternative?(array): string, options considered and not taken
|
||||
consequence?(array): string, what this decision commits the work to
|
||||
context?: string, the situation that prompted the decision
|
||||
affects?(array): Entity, work or notes this decision bears on
|
||||
supersedes?: Entity, a prior decision this one replaces
|
||||
settings:
|
||||
validation: warn
|
||||
frontmatter:
|
||||
status?(enum, lifecycle of the decision): [open, accepted, superseded, rejected]
|
||||
decided?: string, when the decision was made
|
||||
project?: string, the Basic Memory project this decision belongs to
|
||||
---
|
||||
|
||||
# Decision
|
||||
|
||||
A **Decision** note records a real choice with rationale and consequences. Codex
|
||||
uses decisions to avoid relitigating the same tradeoff in later threads.
|
||||
|
||||
Decisions are found by structured recall:
|
||||
`search_notes(metadata_filters={"type": "decision", "status": "open"})`.
|
||||
|
||||
Capture decisions sparingly. Use one note per genuine durable choice.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Task
|
||||
type: schema
|
||||
entity: Task
|
||||
version: 1
|
||||
schema:
|
||||
description: string, what needs to be done
|
||||
status?(enum, current state): [active, blocked, done, abandoned]
|
||||
assigned_to?: string, who is working on this
|
||||
steps?(array): string, ordered steps to complete
|
||||
current_step?: integer, which step number is current
|
||||
context?: string, key context needed to resume
|
||||
started?: string, when work began
|
||||
completed?: string, when work finished
|
||||
blockers?(array): string, what prevents progress
|
||||
parent_task?: Task, parent task if this is a subtask
|
||||
settings:
|
||||
validation: warn
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
A **Task** note tracks work in progress so Codex can find it on the next thread.
|
||||
It matches the framework-agnostic `memory-tasks` shape.
|
||||
|
||||
Tasks are found by structured recall:
|
||||
`search_notes(metadata_filters={"type": "task", "status": "active"})`.
|
||||
|
||||
Put queryable fields such as `status` and `current_step` in frontmatter, and use
|
||||
observations for human-readable progress notes.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: bm-checkpoint
|
||||
description: Save a deliberate Codex work checkpoint to Basic Memory with changed files, verification, decisions, blockers, and the next action.
|
||||
---
|
||||
|
||||
# Checkpoint Codex Work
|
||||
|
||||
Create a durable handoff note for current Codex work. Use this when the user asks
|
||||
to checkpoint, wrap up, hand off, remember the state of the work, or before a long
|
||||
context transition.
|
||||
|
||||
## Gather
|
||||
|
||||
Read `.codex/basic-memory.json` if present:
|
||||
|
||||
- `primaryProject`, default omitted
|
||||
- `captureFolder`, default `codex-sessions`
|
||||
- `placementConventions`, optional
|
||||
|
||||
Gather repo evidence:
|
||||
|
||||
- `git status --short`
|
||||
- current branch
|
||||
- changed files you touched
|
||||
- tests or checks actually run
|
||||
- failures or skipped checks
|
||||
- decisions made in this thread
|
||||
- unresolved blockers
|
||||
- next action
|
||||
|
||||
Do not claim a test passed unless you ran it or the user supplied the result.
|
||||
|
||||
## Write
|
||||
|
||||
Write a note to Basic Memory:
|
||||
|
||||
- `title`: `Codex checkpoint - <short topic>`
|
||||
- `directory`: configured `captureFolder`
|
||||
- `tags`: `["codex", "checkpoint"]`
|
||||
- frontmatter:
|
||||
- `type: codex_session`
|
||||
- `status: open`
|
||||
- `project: <primaryProject if known>`
|
||||
- `cwd: <current cwd>`
|
||||
- `capture: deliberate`
|
||||
|
||||
Use sections:
|
||||
|
||||
- Summary
|
||||
- Changed Files
|
||||
- Verification
|
||||
- Decisions
|
||||
- Blockers
|
||||
- Next Action
|
||||
- Observations
|
||||
|
||||
Observations should include at least one `[next_step]` line. Add relations to
|
||||
existing tasks, decisions, specs, issues, or PRs when the thread has obvious ones.
|
||||
|
||||
## Confirm
|
||||
|
||||
Reply with the permalink and the one next action the checkpoint preserves.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Checkpoint"
|
||||
short_description: "Save a resumable Codex work handoff"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-checkpoint to save the current Codex work state into Basic Memory."
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2.5"/>
|
||||
<path d="M8 3v5h8V3"/>
|
||||
<path d="M8 21v-7h8v7"/>
|
||||
<path d="M10 17h4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: bm-decide
|
||||
description: Capture a durable engineering decision in Basic Memory with rationale, alternatives, consequences, and affected work.
|
||||
---
|
||||
|
||||
# Capture A Decision
|
||||
|
||||
Use this when the user makes or asks to record a durable choice. A decision is a
|
||||
choice with rationale and consequences, not a casual preference.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Resolve `.codex/basic-memory.json`:
|
||||
- write to `primaryProject` when set
|
||||
- follow `placementConventions` for the directory when they are specific
|
||||
- otherwise use `decisions`
|
||||
|
||||
2. Clarify only if the choice itself is ambiguous. Do not ask for every field if
|
||||
the conversation already contains the rationale.
|
||||
|
||||
3. Write a `type: decision` note:
|
||||
- `status: open` unless the user says it is accepted, superseded, or rejected
|
||||
- `decided: <ISO timestamp when known>`
|
||||
- `project: <primaryProject if known>`
|
||||
|
||||
4. Include:
|
||||
- the decision
|
||||
- context
|
||||
- rationale
|
||||
- alternatives considered
|
||||
- consequences
|
||||
- affected files, specs, issues, PRs, or notes
|
||||
|
||||
5. Confirm with the permalink. If this supersedes an older decision, update the old
|
||||
note or link it as `supersedes`.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Decide"
|
||||
short_description: "Record durable engineering decisions"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-decide to capture this engineering decision in Basic Memory."
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
<path d="M8 12.5l2.8 2.8L16.5 9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: bm-orient
|
||||
description: Orient Codex from Basic Memory before substantial repo work by reading active tasks, decisions, recent Codex checkpoints, and repo conventions.
|
||||
---
|
||||
|
||||
# Orient From Basic Memory
|
||||
|
||||
Use this before substantial work in a repo, before resuming an old thread, or when
|
||||
the user asks where things stand.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read `.codex/basic-memory.json` if present. Use `primaryProject`, `secondaryProjects`,
|
||||
`recallTimeframe`, and `placementConventions`. If the file is missing, continue
|
||||
against the default Basic Memory project and mention that setup has not been run.
|
||||
|
||||
2. Query the primary project:
|
||||
- active tasks: `type=task`, `status=active`
|
||||
- open decisions: `type=decision`, `status=open`
|
||||
- recent Codex sessions: `type=codex_session`, after `recallTimeframe`
|
||||
- recent generic sessions only if no Codex sessions are found
|
||||
|
||||
3. Query configured `secondaryProjects` read-only for open decisions. Do not write
|
||||
to shared projects during orientation.
|
||||
|
||||
4. Read the highest-signal hits before summarizing. Prefer notes that match the
|
||||
current repo, named route, issue, branch, or file path.
|
||||
|
||||
5. Present a compact orientation:
|
||||
- active work
|
||||
- decisions that constrain the next move
|
||||
- recent checkpoint cursor
|
||||
- likely next action
|
||||
- any missing setup or ambiguous project mapping
|
||||
|
||||
Keep the summary evidence-backed. Include permalinks for notes you rely on.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Orient"
|
||||
short_description: "Load repo context from Basic Memory"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-orient to load Basic Memory context before changing this repo."
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
<path d="M15.6 8.4l-2.4 5.8-5.8 2.4 2.4-5.8 5.8-2.4z"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: bm-remember
|
||||
description: Quickly save a small fact, reminder, or user preference into Basic Memory from Codex without turning it into a full decision or checkpoint.
|
||||
---
|
||||
|
||||
# Remember
|
||||
|
||||
Use this for lightweight capture: "remember that", "save this", "note this", or
|
||||
a small fact that should survive the current thread.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read `.codex/basic-memory.json` if present:
|
||||
- `primaryProject`, default omitted
|
||||
- `rememberFolder`, default `codex-remember`
|
||||
|
||||
2. Identify the exact text to save. If the user supplied text, preserve their
|
||||
wording. If the user said "remember that" and the referent is unclear, ask one
|
||||
short question.
|
||||
|
||||
3. Write with `write_note`:
|
||||
- `title`: first line trimmed to 80 characters, or a short descriptive title
|
||||
- `directory`: `rememberFolder`
|
||||
- `content`: the text to remember
|
||||
- `tags`: `["codex", "manual-capture"]`
|
||||
- route to `primaryProject` if configured
|
||||
|
||||
4. Confirm in one line with the permalink.
|
||||
|
||||
Do not use this for decisions with alternatives or for work handoffs. Use
|
||||
`bm-decide` or `bm-checkpoint` for those.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Remember"
|
||||
short_description: "Save small facts and preferences"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-remember to save this fact or preference into Basic Memory."
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M7 4.5A2.5 2.5 0 0 1 9.5 2H17v20l-5-3-5 3V4.5z"/>
|
||||
<path d="M10 7h4"/>
|
||||
<path d="M10 11h4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: bm-setup
|
||||
description: Set up Basic Memory for Codex in the current repo by mapping a Basic Memory project, seeding schemas, and writing .codex/basic-memory.json.
|
||||
---
|
||||
|
||||
# Basic Memory for Codex Setup
|
||||
|
||||
Set up the current repo so Codex can orient from Basic Memory and checkpoint work
|
||||
back into it. Keep the interview short, but always ask before choosing where data
|
||||
will be written.
|
||||
|
||||
## Preconditions
|
||||
|
||||
Confirm Basic Memory is reachable before changing files:
|
||||
|
||||
1. Prefer MCP: call `list_memory_projects`.
|
||||
2. If MCP tools are not available, run `basic-memory --version` or `bm --version`.
|
||||
3. If neither works, stop and tell the user to install Basic Memory and connect the
|
||||
MCP server. The plugin bundles an `.mcp.json` that starts `uvx basic-memory mcp`.
|
||||
4. List available projects before the interview. Include cloud/local source,
|
||||
workspace, qualified name, and project id when available.
|
||||
|
||||
## Interview
|
||||
|
||||
Ask the user to choose the project mapping. Do not infer write targets from the
|
||||
repo, default project, current directory, or previous local state.
|
||||
|
||||
- storage mode: cloud, local, or mixed. Prefer the user's stated mode over any
|
||||
CLI default.
|
||||
- `focus`: code/dev, research, writing, planning, or mixed.
|
||||
- `primaryProject`: an existing Basic Memory project or a new one to create.
|
||||
- `secondaryProjects`: optional read-only projects for session-start context.
|
||||
- `teamProjects`: optional share targets for `bm-share`.
|
||||
- `captureFolder`: default `codex-sessions`.
|
||||
- `rememberFolder`: default `codex-remember`.
|
||||
- `placementConventions`: a short note about where decisions, tasks, and research
|
||||
notes should land.
|
||||
|
||||
If there are duplicate names, show qualified names and ask the user which one to
|
||||
use. Prefer qualified project names or project ids for cloud projects. Never pick
|
||||
between cloud and local variants without confirmation.
|
||||
|
||||
For a new or empty project, suggest a light convention instead of creating empty
|
||||
folders. For an existing project, inspect `list_directory` and a few notes before
|
||||
summarizing the real convention.
|
||||
|
||||
## Apply
|
||||
|
||||
After confirming the plan, write `.codex/basic-memory.json` in the repo:
|
||||
|
||||
```json
|
||||
{
|
||||
"basicMemory": {
|
||||
"primaryProject": "<project-ref>",
|
||||
"secondaryProjects": [],
|
||||
"projectMode": "cloud",
|
||||
"teamProjects": {},
|
||||
"focus": "<focus>",
|
||||
"captureFolder": "codex-sessions",
|
||||
"rememberFolder": "codex-remember",
|
||||
"recallTimeframe": "7d",
|
||||
"placementConventions": "<short convention>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Preserve unrelated keys if the file already exists. Include `projectMode` when
|
||||
the user chose cloud, local, or mixed routing. This file is intentionally
|
||||
Codex-specific; do not write `.claude/settings.json`.
|
||||
|
||||
## Seed Schemas
|
||||
|
||||
Read the schema files from `<plugin-root>/schemas/`. This skill lives at
|
||||
`<plugin-root>/skills/bm-setup/SKILL.md`, so the schemas are two directories up.
|
||||
|
||||
Seed these schema notes into the chosen `primaryProject` if they do not already
|
||||
exist:
|
||||
|
||||
- `codex-session.md`
|
||||
- `decision.md`
|
||||
- `task.md`
|
||||
|
||||
Use `write_note` with `directory="schemas"`, `note_type="schema"`, schema
|
||||
frontmatter as metadata, and the markdown body as content. Do not paste the YAML
|
||||
frontmatter into content.
|
||||
|
||||
Before seeding schemas, restate the exact target project and ask for confirmation
|
||||
if it differs from the user's selected primary project or if routing is
|
||||
ambiguous.
|
||||
|
||||
## Verify
|
||||
|
||||
Before closing, prove the mapping works:
|
||||
|
||||
- Search the primary project for `type=schema` with page size 5.
|
||||
- Search one shared project for open decisions if shared projects were configured.
|
||||
- If either query errors, fix the project ref before finishing.
|
||||
|
||||
Finish with the project mapping, schemas seeded or skipped, and the verification
|
||||
result. Tell the user that plugin hooks need to be reviewed and trusted in Codex
|
||||
before they run.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Setup"
|
||||
short_description: "Map this repo to Basic Memory"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-setup to map this repo to the right Basic Memory project."
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 6h7"/>
|
||||
<path d="M15 6h5"/>
|
||||
<circle cx="13" cy="6" r="2"/>
|
||||
<path d="M4 18h5"/>
|
||||
<path d="M13 18h7"/>
|
||||
<circle cx="11" cy="18" r="2"/>
|
||||
<path d="M4 12h3"/>
|
||||
<path d="M11 12h9"/>
|
||||
<circle cx="9" cy="12" r="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: bm-share
|
||||
description: Share a personal Basic Memory note to a configured team project from Codex with attribution and explicit confirmation.
|
||||
---
|
||||
|
||||
# Share A Note
|
||||
|
||||
Copy a note from the configured primary project to a configured team project. This
|
||||
is the deliberate shared-write path. Automatic checkpoints and quick remembers
|
||||
stay personal.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read `.codex/basic-memory.json` and resolve:
|
||||
- `primaryProject`
|
||||
- `teamProjects`, a map of project ref to settings
|
||||
|
||||
2. If no team projects are configured, stop and ask the user to run setup or add a
|
||||
target. Do not invent a team destination.
|
||||
|
||||
3. Read the source note from the user's argument or the current conversation. If
|
||||
ambiguous, ask which note to share.
|
||||
|
||||
4. Pick the target. If there is more than one team project, ask which one.
|
||||
|
||||
5. Confirm before writing. The prompt should be specific:
|
||||
`Share "<title>" to <target>/<promoteFolder>?`
|
||||
|
||||
6. Write the copy:
|
||||
- route to the target project
|
||||
- `directory`: target `promoteFolder`, default `shared`
|
||||
- preserve the original content and useful frontmatter
|
||||
- add `shared_from: <source permalink>` frontmatter when possible
|
||||
- add `- [context] Shared from <source permalink>` as an observation
|
||||
|
||||
7. Confirm with the new team permalink.
|
||||
|
||||
Never share secrets, credentials, or private notes without an explicit yes.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Share"
|
||||
short_description: "Copy notes to configured team projects"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-share to share this Basic Memory note with a configured team project."
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3"/>
|
||||
<circle cx="6" cy="12" r="3"/>
|
||||
<circle cx="18" cy="19" r="3"/>
|
||||
<path d="M8.6 10.6l5.8-3.2"/>
|
||||
<path d="M8.6 13.4l5.8 3.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 352 B |
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: bm-status
|
||||
description: Report the Basic Memory for Codex configuration, reachability, hook expectations, recent Codex checkpoints, and active tasks.
|
||||
---
|
||||
|
||||
# Basic Memory For Codex Status
|
||||
|
||||
Gather a concise diagnostic. Do not over-investigate.
|
||||
|
||||
## Gather
|
||||
|
||||
1. CLI reachability:
|
||||
- `basic-memory --version`
|
||||
- fallback `bm --version`
|
||||
|
||||
2. Plugin config:
|
||||
- read `.codex/basic-memory.json`
|
||||
- report `primaryProject`, `secondaryProjects`, `teamProjects`,
|
||||
`captureFolder`, `rememberFolder`, `recallTimeframe`, and `focus`
|
||||
|
||||
3. Hook files:
|
||||
- confirm `plugins/codex/hooks/hooks.json` exists if running from this repo
|
||||
- remind the user that Codex plugin hooks must be reviewed and trusted before
|
||||
they run
|
||||
|
||||
4. Basic Memory queries:
|
||||
- recent `type=codex_session`, page size 5
|
||||
- active `type=task`, `status=active`
|
||||
- open `type=decision`, `status=open`
|
||||
|
||||
## Present
|
||||
|
||||
Use this shape:
|
||||
|
||||
```text
|
||||
Basic Memory for Codex
|
||||
- CLI: <version or missing>
|
||||
- Project: <primaryProject or default>
|
||||
- Reads from: <secondaryProjects or none>
|
||||
- Share targets: <teamProjects or none>
|
||||
- Capture folder: <captureFolder>
|
||||
- Remember folder: <rememberFolder>
|
||||
- Recall timeframe: <recallTimeframe>
|
||||
- Recent Codex checkpoints: <count>
|
||||
- Active tasks: <count>
|
||||
- Open decisions: <count>
|
||||
- Hooks: installed; trust review required in Codex
|
||||
```
|
||||
|
||||
List recent checkpoints by title and permalink when available.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Status"
|
||||
short_description: "Check Basic Memory plugin health"
|
||||
icon_small: "./assets/icon.svg"
|
||||
icon_large: "./assets/icon.svg"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $bm-status to report the Basic Memory for Codex configuration and health."
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#111827" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h4l2-6 4 12 2-6h6"/>
|
||||
<path d="M4 20h16"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 248 B |
@@ -48,8 +48,13 @@ dependencies = [
|
||||
"fastembed>=0.7.4",
|
||||
"sqlite-vec>=0.1.6",
|
||||
"openai>=1.100.2",
|
||||
"litellm>=1.60.0,<2.0.0",
|
||||
"logfire>=4.19.0",
|
||||
"psutil>=5.9.0",
|
||||
# uvloop's C event loop has no self._ready.popleft() codepath, so the
|
||||
# asyncpg engine-dispose race ("IndexError: pop from an empty deque") that
|
||||
# crashes the Postgres backend cannot fire under it. Not available on Windows.
|
||||
"uvloop>=0.21.0; sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -71,6 +76,10 @@ addopts = "--cov=basic_memory --cov-report term-missing"
|
||||
testpaths = ["tests", "test-int"]
|
||||
asyncio_mode = "strict"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
# Any test hanging >120s fails with a stack dump instead of stalling the CI job
|
||||
# until the runner times out (the FastMCP/asyncpg cleanup-hang family).
|
||||
timeout = 120
|
||||
timeout_method = "thread"
|
||||
filterwarnings = [
|
||||
"ignore:The @wait_container_is_ready decorator is deprecated.*:DeprecationWarning:testcontainers\\.core\\.waiting_utils",
|
||||
"ignore:The default datetime adapter is deprecated as of Python 3\\.12.*:DeprecationWarning:aiosqlite\\.core",
|
||||
@@ -84,6 +93,7 @@ markers = [
|
||||
"windows: Windows-specific tests (deselect with '-m \"not windows\"')",
|
||||
"smoke: Fast end-to-end smoke tests for MCP flows",
|
||||
"semantic: Tests requiring semantic dependencies (fastembed, sqlite-vec, openai)",
|
||||
"live: Tests that call external provider APIs and require explicit opt-in",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
@@ -109,6 +119,7 @@ dev = [
|
||||
"ty>=0.0.18",
|
||||
"cst-lsp>=0.1.3",
|
||||
"libcst>=1.8.6",
|
||||
"pytest-timeout>=2.4.0",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
"""BM Bossbot status and PR-body helpers.
|
||||
|
||||
BM Bossbot is a deterministic merge gate — no LLM review. It approves a head
|
||||
SHA only when the Tests workflow succeeded for it (enforced by the workflow
|
||||
trigger), the PR is not a draft, the author is trusted, and every review
|
||||
thread is resolved. Code review itself comes from the Codex connector and
|
||||
human reviewers; this gate just refuses to let unaddressed feedback merge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Mapping
|
||||
|
||||
import typer
|
||||
|
||||
|
||||
STATUS_CONTEXT = "BM Bossbot Approval"
|
||||
SUMMARY_START = "<!-- BM_BOSSBOT_SUMMARY:start -->"
|
||||
SUMMARY_END = "<!-- BM_BOSSBOT_SUMMARY:end -->"
|
||||
APPROVED_DESCRIPTION = "BM Bossbot approved this head SHA"
|
||||
PENDING_DESCRIPTION = "BM Bossbot is reviewing this head SHA"
|
||||
app = typer.Typer(
|
||||
add_completion=False,
|
||||
help="Manage deterministic BM Bossbot PR approval statuses.",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApprovalResult:
|
||||
approved: bool
|
||||
state: str
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PullRequestEvent:
|
||||
repo: str
|
||||
number: int
|
||||
head_sha: str
|
||||
body: str
|
||||
|
||||
|
||||
def read_json(path: Path) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"Missing JSON file: {path}") from None
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit(f"{path}: invalid JSON: {exc}") from None
|
||||
|
||||
|
||||
def pull_request_event(
|
||||
payload: Mapping[str, Any], repo_override: str | None = None
|
||||
) -> PullRequestEvent:
|
||||
pr = payload.get("pull_request")
|
||||
if not isinstance(pr, Mapping):
|
||||
raise SystemExit("GitHub event payload is missing pull_request")
|
||||
|
||||
repo = repo_override
|
||||
if repo is None:
|
||||
repository = payload.get("repository")
|
||||
if isinstance(repository, Mapping):
|
||||
repo = _string(repository.get("full_name"))
|
||||
if not repo:
|
||||
raise SystemExit("Could not determine GitHub repository")
|
||||
|
||||
number = pr.get("number")
|
||||
if not isinstance(number, int):
|
||||
raise SystemExit("GitHub event payload is missing pull_request.number")
|
||||
|
||||
head = pr.get("head")
|
||||
head_sha = (
|
||||
_string(head.get("sha")) if isinstance(head, Mapping) else _string(pr.get("head_sha"))
|
||||
)
|
||||
if not head_sha:
|
||||
raise SystemExit("GitHub event payload is missing pull_request.head.sha")
|
||||
|
||||
return PullRequestEvent(
|
||||
repo=repo,
|
||||
number=number,
|
||||
head_sha=head_sha,
|
||||
body=_string(pr.get("body")),
|
||||
)
|
||||
|
||||
|
||||
def count_unresolved_review_threads(*, token: str, repo: str, number: int) -> int:
|
||||
"""Count unresolved review threads (e.g. open Codex inline comments) on a PR.
|
||||
|
||||
Review threads are the canonical 'outstanding feedback' signal: bot reviewers
|
||||
submit COMMENTED reviews that never flip reviewDecision, so thread resolution
|
||||
is the only deterministic way to know feedback was addressed.
|
||||
"""
|
||||
owner, _, name = repo.partition("/")
|
||||
if not owner or not name:
|
||||
raise SystemExit(f"Invalid repository: {repo}")
|
||||
|
||||
query = """
|
||||
query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
pullRequest(number: $number) {
|
||||
reviewThreads(first: 100, after: $cursor) {
|
||||
pageInfo { hasNextPage endCursor }
|
||||
nodes { isResolved }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
unresolved = 0
|
||||
cursor: str | None = None
|
||||
while True:
|
||||
response = _github_request(
|
||||
method="POST",
|
||||
path="/graphql",
|
||||
token=token,
|
||||
payload={
|
||||
"query": query,
|
||||
"variables": {"owner": owner, "name": name, "number": number, "cursor": cursor},
|
||||
},
|
||||
)
|
||||
if not isinstance(response, Mapping) or response.get("errors"):
|
||||
raise SystemExit(f"GitHub GraphQL reviewThreads query failed: {response}")
|
||||
try:
|
||||
threads = response["data"]["repository"]["pullRequest"]["reviewThreads"]
|
||||
nodes = threads["nodes"]
|
||||
page_info = threads["pageInfo"]
|
||||
except (KeyError, TypeError):
|
||||
raise SystemExit(
|
||||
"GitHub GraphQL reviewThreads response was missing expected fields"
|
||||
) from None
|
||||
unresolved += sum(1 for node in nodes if not node.get("isResolved"))
|
||||
if not page_info.get("hasNextPage"):
|
||||
return unresolved
|
||||
cursor = page_info.get("endCursor")
|
||||
|
||||
|
||||
def unresolved_threads_result(count: int) -> ApprovalResult:
|
||||
return ApprovalResult(
|
||||
False,
|
||||
"failure",
|
||||
f"BM Bossbot found {count} unresolved review thread(s)",
|
||||
)
|
||||
|
||||
|
||||
def evaluate_gate(
|
||||
*,
|
||||
token: str,
|
||||
repo: str,
|
||||
number: int,
|
||||
trusted: bool,
|
||||
) -> tuple[ApprovalResult, int]:
|
||||
"""Deterministic approval decision: trusted author + zero unresolved threads.
|
||||
|
||||
Tests-passed-for-this-head and non-draft are enforced upstream by the
|
||||
workflow trigger and the normalize step (should_review). Returns the
|
||||
result plus the unresolved-thread count for the PR-body summary.
|
||||
"""
|
||||
if not trusted:
|
||||
return (
|
||||
ApprovalResult(
|
||||
False,
|
||||
"failure",
|
||||
"BM Bossbot only gates owner/member/collaborator PRs",
|
||||
),
|
||||
0,
|
||||
)
|
||||
unresolved = count_unresolved_review_threads(token=token, repo=repo, number=number)
|
||||
if unresolved > 0:
|
||||
return unresolved_threads_result(unresolved), unresolved
|
||||
return ApprovalResult(True, "success", APPROVED_DESCRIPTION), 0
|
||||
|
||||
|
||||
def build_status_payload(*, state: str, description: str, target_url: str) -> dict[str, str]:
|
||||
return {
|
||||
"state": state,
|
||||
"context": STATUS_CONTEXT,
|
||||
"description": description,
|
||||
"target_url": target_url,
|
||||
}
|
||||
|
||||
|
||||
def render_summary(
|
||||
*,
|
||||
head_sha: str,
|
||||
result: ApprovalResult,
|
||||
trusted: bool,
|
||||
unresolved_threads: int,
|
||||
) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
f"Reviewed SHA: `{head_sha}`",
|
||||
"Gate: deterministic (tests, draft, author trust, review threads)",
|
||||
f"Status: `{result.state}` - {result.description}",
|
||||
"",
|
||||
f"- Trusted author: {'yes' if trusted else 'no'}",
|
||||
f"- Unresolved review threads: {unresolved_threads}",
|
||||
"",
|
||||
"Code review comes from the Codex connector and human reviewers;",
|
||||
"resolve every review thread to (re)gain approval for this head SHA.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def upsert_summary_block(body: str, summary: str) -> str:
|
||||
block = f"{SUMMARY_START}\n{summary.rstrip()}\n{SUMMARY_END}"
|
||||
pattern = re.compile(
|
||||
rf"{re.escape(SUMMARY_START)}.*?{re.escape(SUMMARY_END)}",
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if pattern.search(body):
|
||||
return pattern.sub(block, body, count=1)
|
||||
if body.strip():
|
||||
return f"{body.rstrip()}\n\n{block}\n"
|
||||
return f"{block}\n"
|
||||
|
||||
|
||||
def set_commit_status(*, token: str, repo: str, sha: str, payload: Mapping[str, str]) -> None:
|
||||
_github_request(
|
||||
method="POST",
|
||||
path=f"/repos/{repo}/statuses/{sha}",
|
||||
token=token,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
def update_pull_request_body(*, token: str, repo: str, number: int, body: str) -> None:
|
||||
_github_request(
|
||||
method="PATCH",
|
||||
path=f"/repos/{repo}/pulls/{number}",
|
||||
token=token,
|
||||
payload={"body": body},
|
||||
)
|
||||
|
||||
|
||||
def get_pull_request_body(*, token: str, repo: str, number: int) -> str:
|
||||
response = _github_request(
|
||||
method="GET",
|
||||
path=f"/repos/{repo}/pulls/{number}",
|
||||
token=token,
|
||||
)
|
||||
if not isinstance(response, Mapping):
|
||||
raise SystemExit("GitHub API response for pull request was invalid")
|
||||
return _string(response.get("body"))
|
||||
|
||||
|
||||
def mark_pending(
|
||||
*,
|
||||
event_path: Path,
|
||||
repo: str | None,
|
||||
run_url: str,
|
||||
token_env: str,
|
||||
) -> None:
|
||||
event = pull_request_event(read_json(event_path), repo_override=repo)
|
||||
set_commit_status(
|
||||
token=_token(token_env),
|
||||
repo=event.repo,
|
||||
sha=event.head_sha,
|
||||
payload=build_status_payload(
|
||||
state="pending",
|
||||
description=PENDING_DESCRIPTION,
|
||||
target_url=run_url,
|
||||
),
|
||||
)
|
||||
typer.echo(f"Marked {STATUS_CONTEXT} pending for {event.head_sha}")
|
||||
|
||||
|
||||
def finalize_review(
|
||||
*,
|
||||
event_path: Path,
|
||||
trusted: bool,
|
||||
repo: str | None,
|
||||
run_url: str,
|
||||
token_env: str,
|
||||
) -> ApprovalResult:
|
||||
event = pull_request_event(read_json(event_path), repo_override=repo)
|
||||
token = _token(token_env)
|
||||
|
||||
result, unresolved = evaluate_gate(
|
||||
token=token, repo=event.repo, number=event.number, trusted=trusted
|
||||
)
|
||||
current_body = get_pull_request_body(token=token, repo=event.repo, number=event.number)
|
||||
updated_body = upsert_summary_block(
|
||||
current_body,
|
||||
render_summary(
|
||||
head_sha=event.head_sha,
|
||||
result=result,
|
||||
trusted=trusted,
|
||||
unresolved_threads=unresolved,
|
||||
),
|
||||
)
|
||||
update_pull_request_body(token=token, repo=event.repo, number=event.number, body=updated_body)
|
||||
set_commit_status(
|
||||
token=token,
|
||||
repo=event.repo,
|
||||
sha=event.head_sha,
|
||||
payload=build_status_payload(
|
||||
state=result.state,
|
||||
description=result.description,
|
||||
target_url=run_url,
|
||||
),
|
||||
)
|
||||
typer.echo(f"Marked {STATUS_CONTEXT} {result.state} for {event.head_sha}")
|
||||
return result
|
||||
|
||||
|
||||
def get_pull_request_head_sha(*, token: str, repo: str, number: int) -> str:
|
||||
response = _github_request(
|
||||
method="GET",
|
||||
path=f"/repos/{repo}/pulls/{number}",
|
||||
token=token,
|
||||
)
|
||||
if not isinstance(response, Mapping):
|
||||
raise SystemExit("GitHub API response for pull request was invalid")
|
||||
head = response.get("head")
|
||||
head_sha = _string(head.get("sha")) if isinstance(head, Mapping) else ""
|
||||
if not head_sha:
|
||||
raise SystemExit("GitHub API response was missing pull request head SHA")
|
||||
return head_sha
|
||||
|
||||
|
||||
def head_sha_was_approved(*, token: str, repo: str, sha: str) -> bool:
|
||||
"""Return whether a full BM Bossbot review previously approved this head SHA.
|
||||
|
||||
Commit statuses are append-only history, so the approval record survives a
|
||||
later thread-failure status for the same SHA. The recheck path can post a
|
||||
new status on every review-thread event, so a busy PR can accumulate more
|
||||
than one page of statuses — page through all of them or the approval
|
||||
record falls off page one and a valid approval is never restored.
|
||||
"""
|
||||
page = 1
|
||||
while True:
|
||||
response = _github_request(
|
||||
method="GET",
|
||||
path=f"/repos/{repo}/commits/{sha}/statuses?per_page=100&page={page}",
|
||||
token=token,
|
||||
)
|
||||
if not isinstance(response, list):
|
||||
raise SystemExit("GitHub API response for commit statuses was invalid")
|
||||
if not response:
|
||||
return False
|
||||
if any(
|
||||
isinstance(status, Mapping)
|
||||
and status.get("context") == STATUS_CONTEXT
|
||||
and status.get("state") == "success"
|
||||
and status.get("description") == APPROVED_DESCRIPTION
|
||||
for status in response
|
||||
):
|
||||
return True
|
||||
page += 1
|
||||
|
||||
|
||||
def recheck_threads(
|
||||
*,
|
||||
repo: str,
|
||||
number: int,
|
||||
run_url: str,
|
||||
token_env: str,
|
||||
) -> None:
|
||||
"""Re-evaluate the approval status when review threads change.
|
||||
|
||||
Trigger: pull_request_review / review_comment / review_thread events.
|
||||
Why: the full review runs once per head SHA after Tests; feedback that
|
||||
arrives later (or gets resolved later) must move the gate without
|
||||
re-running the LLM review.
|
||||
Outcome: unresolved threads flip the status to failure; once every thread
|
||||
is resolved, a previously earned approval for the same head SHA is
|
||||
restored. Without a prior approval the status is left untouched so a
|
||||
pending/failed review cannot be upgraded by thread resolution alone.
|
||||
"""
|
||||
token = _token(token_env)
|
||||
head_sha = get_pull_request_head_sha(token=token, repo=repo, number=number)
|
||||
unresolved = count_unresolved_review_threads(token=token, repo=repo, number=number)
|
||||
|
||||
if unresolved > 0:
|
||||
result = unresolved_threads_result(unresolved)
|
||||
elif head_sha_was_approved(token=token, repo=repo, sha=head_sha):
|
||||
result = ApprovalResult(True, "success", APPROVED_DESCRIPTION)
|
||||
else:
|
||||
typer.echo(
|
||||
f"All review threads resolved but no prior approval exists for {head_sha}; "
|
||||
"leaving status unchanged"
|
||||
)
|
||||
return
|
||||
|
||||
set_commit_status(
|
||||
token=token,
|
||||
repo=repo,
|
||||
sha=head_sha,
|
||||
payload=build_status_payload(
|
||||
state=result.state,
|
||||
description=result.description,
|
||||
target_url=run_url,
|
||||
),
|
||||
)
|
||||
typer.echo(f"Marked {STATUS_CONTEXT} {result.state} for {head_sha} ({result.description})")
|
||||
|
||||
|
||||
def _github_request(
|
||||
*,
|
||||
method: str,
|
||||
path: str,
|
||||
token: str,
|
||||
payload: Mapping[str, Any] | None = None,
|
||||
) -> Any:
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
f"https://api.github.com{path}",
|
||||
data=data,
|
||||
method=method,
|
||||
headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "basic-memory-bm-bossbot",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
response_body = response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(f"GitHub API request failed: {exc.code} {detail}") from None
|
||||
return json.loads(response_body) if response_body else None
|
||||
|
||||
|
||||
def _string(value: object) -> str:
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _token(env_name: str) -> str:
|
||||
token = os.environ.get(env_name)
|
||||
if not token:
|
||||
raise SystemExit(f"Missing required token environment variable: {env_name}")
|
||||
return token
|
||||
|
||||
|
||||
@app.command("pending")
|
||||
def pending(
|
||||
event: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--event",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="GitHub event payload JSON.",
|
||||
),
|
||||
],
|
||||
run_url: Annotated[str, typer.Option("--run-url", help="Workflow run URL.")],
|
||||
repo: Annotated[str | None, typer.Option("--repo", help="owner/name repository.")] = None,
|
||||
token_env: Annotated[
|
||||
str,
|
||||
typer.Option("--token-env", help="Environment variable containing a GitHub token."),
|
||||
] = "GITHUB_TOKEN",
|
||||
) -> None:
|
||||
"""Set BM Bossbot Approval pending on the PR head SHA."""
|
||||
mark_pending(event_path=event, repo=repo, run_url=run_url, token_env=token_env)
|
||||
|
||||
|
||||
@app.command("finalize")
|
||||
def finalize(
|
||||
event: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--event",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="GitHub event payload JSON.",
|
||||
),
|
||||
],
|
||||
trusted: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--trusted",
|
||||
help="Whether the PR author is trusted (true/false from the classify step).",
|
||||
),
|
||||
],
|
||||
run_url: Annotated[str, typer.Option("--run-url", help="Workflow run URL.")],
|
||||
repo: Annotated[str | None, typer.Option("--repo", help="owner/name repository.")] = None,
|
||||
token_env: Annotated[
|
||||
str,
|
||||
typer.Option("--token-env", help="Environment variable containing a GitHub token."),
|
||||
] = "GITHUB_TOKEN",
|
||||
) -> None:
|
||||
"""Finalize BM Bossbot Approval from the deterministic gate."""
|
||||
result = finalize_review(
|
||||
event_path=event,
|
||||
trusted=trusted.strip().lower() == "true",
|
||||
repo=repo,
|
||||
run_url=run_url,
|
||||
token_env=token_env,
|
||||
)
|
||||
if not result.approved:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("recheck")
|
||||
def recheck(
|
||||
pr_number: Annotated[int, typer.Option("--pr-number", min=1, help="Pull request number.")],
|
||||
run_url: Annotated[str, typer.Option("--run-url", help="Workflow run URL.")],
|
||||
repo: Annotated[str, typer.Option("--repo", help="owner/name repository.")],
|
||||
token_env: Annotated[
|
||||
str,
|
||||
typer.Option("--token-env", help="Environment variable containing a GitHub token."),
|
||||
] = "GITHUB_TOKEN",
|
||||
) -> None:
|
||||
"""Re-evaluate BM Bossbot Approval from current review-thread state."""
|
||||
recheck_threads(repo=repo, number=pr_number, run_url=run_url, token_env=token_env)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "openai>=1.100.2",
|
||||
# "python-dotenv>=1.1.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
"""Generate a BM Bossbot infographic with the OpenAI Images API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
DEFAULT_MODEL = "gpt-image-2"
|
||||
DEFAULT_SIZE = "1536x1024"
|
||||
DEFAULT_QUALITY = "high"
|
||||
DEFAULT_FORMAT = "webp"
|
||||
DEFAULT_COMPRESSION = 90
|
||||
app = typer.Typer(
|
||||
add_completion=False,
|
||||
help="Generate Basic Memory infographics with the OpenAI Images API.",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GeneratedImage:
|
||||
path: Path
|
||||
revised_prompt: str | None
|
||||
|
||||
|
||||
def validate_output_path(path: Path, *, repo_root: Path | None = None) -> Path:
|
||||
root = (repo_root or Path.cwd()).resolve()
|
||||
output = path.resolve()
|
||||
allowed_root = (root / "docs" / "assets" / "infographics").resolve()
|
||||
if not output.is_relative_to(allowed_root):
|
||||
allowed_path = allowed_root.relative_to(root).as_posix()
|
||||
raise ValueError(f"Output path must be under {allowed_path}")
|
||||
if output.suffix != ".webp":
|
||||
raise ValueError("Output path must end with .webp")
|
||||
return output
|
||||
|
||||
|
||||
def generate_image_result(
|
||||
*,
|
||||
prompt: str,
|
||||
output_path: Path,
|
||||
model: str = DEFAULT_MODEL,
|
||||
size: str = DEFAULT_SIZE,
|
||||
quality: str = DEFAULT_QUALITY,
|
||||
output_format: str = DEFAULT_FORMAT,
|
||||
output_compression: int = DEFAULT_COMPRESSION,
|
||||
client: Any | None = None,
|
||||
retries: int = 2,
|
||||
) -> GeneratedImage:
|
||||
output = validate_output_path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
load_dotenv()
|
||||
openai_client = client or OpenAI()
|
||||
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
response = openai_client.images.generate(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
size=size,
|
||||
quality=quality,
|
||||
output_format=output_format,
|
||||
output_compression=output_compression,
|
||||
)
|
||||
image = response.data[0]
|
||||
image_b64 = image.b64_json
|
||||
if not image_b64:
|
||||
raise RuntimeError("OpenAI image response did not include b64_json")
|
||||
output.write_bytes(base64.b64decode(image_b64))
|
||||
return GeneratedImage(path=output, revised_prompt=image.revised_prompt)
|
||||
except Exception:
|
||||
if attempt >= retries:
|
||||
raise
|
||||
time.sleep(2**attempt)
|
||||
|
||||
raise RuntimeError("Image generation retry loop exited unexpectedly")
|
||||
|
||||
|
||||
def generate_image(
|
||||
*,
|
||||
prompt: str,
|
||||
output_path: Path,
|
||||
model: str = DEFAULT_MODEL,
|
||||
size: str = DEFAULT_SIZE,
|
||||
quality: str = DEFAULT_QUALITY,
|
||||
output_format: str = DEFAULT_FORMAT,
|
||||
output_compression: int = DEFAULT_COMPRESSION,
|
||||
client: Any | None = None,
|
||||
retries: int = 2,
|
||||
) -> Path:
|
||||
return generate_image_result(
|
||||
prompt=prompt,
|
||||
output_path=output_path,
|
||||
model=model,
|
||||
size=size,
|
||||
quality=quality,
|
||||
output_format=output_format,
|
||||
output_compression=output_compression,
|
||||
client=client,
|
||||
retries=retries,
|
||||
).path
|
||||
|
||||
|
||||
@app.command()
|
||||
def generate(
|
||||
prompt_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--prompt-file",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="Markdown/text prompt file to send to the image model.",
|
||||
),
|
||||
],
|
||||
output: Annotated[Path, typer.Option("--output", help="Output .webp path.")],
|
||||
model: Annotated[str, typer.Option("--model", help="OpenAI image model.")] = DEFAULT_MODEL,
|
||||
size: Annotated[str, typer.Option("--size", help="Image size.")] = DEFAULT_SIZE,
|
||||
quality: Annotated[str, typer.Option("--quality", help="Image quality.")] = DEFAULT_QUALITY,
|
||||
output_compression: Annotated[
|
||||
int,
|
||||
typer.Option(
|
||||
"--output-compression",
|
||||
min=0,
|
||||
max=100,
|
||||
help="WebP output compression.",
|
||||
),
|
||||
] = DEFAULT_COMPRESSION,
|
||||
retries: Annotated[int, typer.Option("--retries", min=0, help="Retry attempts.")] = 2,
|
||||
) -> None:
|
||||
"""Generate an infographic from a prompt file."""
|
||||
output = generate_image(
|
||||
prompt=prompt_file.read_text(encoding="utf-8"),
|
||||
output_path=output,
|
||||
model=model,
|
||||
size=size,
|
||||
quality=quality,
|
||||
output_compression=output_compression,
|
||||
retries=retries,
|
||||
)
|
||||
typer.echo(output)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "openai>=1.100.2",
|
||||
# "python-dotenv>=1.1.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
"""Build and generate a non-gating BM Bossbot PR image."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Mapping
|
||||
|
||||
import typer
|
||||
|
||||
if __package__:
|
||||
from .generate_infographic import (
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_SIZE,
|
||||
generate_image_result,
|
||||
)
|
||||
else:
|
||||
from generate_infographic import (
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_SIZE,
|
||||
generate_image_result,
|
||||
)
|
||||
|
||||
|
||||
SUMMARY_START = "<!-- BM_BOSSBOT_SUMMARY:start -->"
|
||||
SUMMARY_END = "<!-- BM_BOSSBOT_SUMMARY:end -->"
|
||||
THEME_START = "<!-- BM_INFOGRAPHIC_THEME:start -->"
|
||||
THEME_END = "<!-- BM_INFOGRAPHIC_THEME:end -->"
|
||||
PROVENANCE_START = "<!-- BM_INFOGRAPHIC_PROVENANCE:start -->"
|
||||
PROVENANCE_END = "<!-- BM_INFOGRAPHIC_PROVENANCE:end -->"
|
||||
IMAGE_START = "<!-- pr-infographic:start -->"
|
||||
IMAGE_END = "<!-- pr-infographic:end -->"
|
||||
# Managed blocks are bot-written artifacts (review verdict, image embed,
|
||||
# provenance). They must never feed the image: sourcing the review summary is
|
||||
# what made every image an "APPROVED" stamp instead of depicting the change.
|
||||
MANAGED_BLOCKS = (
|
||||
(SUMMARY_START, SUMMARY_END),
|
||||
(THEME_START, THEME_END),
|
||||
(PROVENANCE_START, PROVENANCE_END),
|
||||
(IMAGE_START, IMAGE_END),
|
||||
)
|
||||
app = typer.Typer(
|
||||
add_completion=False,
|
||||
help="Generate a non-gating BM Bossbot PR image.",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
class ThemeSource(StrEnum):
|
||||
AUTO = "auto"
|
||||
CLI = "cli"
|
||||
PR_BODY = "pr-body"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThemeSelection:
|
||||
theme: str
|
||||
source: ThemeSource
|
||||
|
||||
|
||||
BM_IMAGE_THEME_POOL = (
|
||||
"computer science college textbook: SICP-style diagrams, automata, compiler "
|
||||
"pipelines, type theory, and annotated chalkboard rigor",
|
||||
"classic literature: sea voyages, gothic manors, Dickensian streets, library "
|
||||
"marginalia, and travel-journal artifacts",
|
||||
"fantasy quest ledger: original guild maps, spellbooks, dungeon keys, tavern "
|
||||
"notices, and artifact inventories with no copyrighted settings",
|
||||
"heavy music editorial: metal, hard rock, punk, techno, soul, or reggae "
|
||||
"tour-poster energy with no direct band logos or likenesses",
|
||||
"knockoff space opera: fleet routes, mission consoles, contraband manifests, "
|
||||
"and practical starship drama with no named fictional universes",
|
||||
"sword-and-sorcery: ruined temples, desert roads, battle standards, ancient "
|
||||
"maps, and heroic silhouettes with no named character likenesses",
|
||||
"comic book cover: original splash-page composition, caption boxes, clean "
|
||||
"halftone texture, and bold issue-cover drama",
|
||||
"French new wave movie poster: stark typography, city streets, jump-cut "
|
||||
"composition, and high-contrast editorial photography cues",
|
||||
"WWII public-information poster: home-front logistics, mobilization arrows, "
|
||||
"bold simplified figures, and no real-world party symbols or hate imagery",
|
||||
"Italian movie poster: hand-painted drama, expressive color, credit-block "
|
||||
"energy, and 1960s or 1970s cinema composition with no actor likenesses",
|
||||
"Shakespearean stage: acts and scenes, court intrigue, stage blocking, "
|
||||
"dramatis personae, backstage cue sheets, and theatrical light",
|
||||
"Greek mythology: temple steps, oracle tablets, constellations, labyrinths, "
|
||||
"ship routes, and original heroic allegory",
|
||||
"noir detective photography: case files, typed evidence labels, civic "
|
||||
"infrastructure, streetlight shadows, and newsroom archive grit",
|
||||
"space exploration and astronomy: celestial atlases, observatory charts, "
|
||||
"orbital mechanics, planetary survey routes, and deep-space mission drama",
|
||||
"editorial painting: abstract, classical landscape, western action, "
|
||||
"chiaroscuro, historical mural, stormy seascape, or allegorical canvas",
|
||||
"classic black-and-white photography: documentary field report, contact "
|
||||
"sheet, street photography, civic infrastructure, and darkroom contrast",
|
||||
"80's action movie poster: smoky backlit warehouses, neon streets, practical "
|
||||
"explosions, mission dossiers, countdowns, and no actor likenesses",
|
||||
"alchemy manuscript: transformation diagrams, annotated symbols, recipe-like "
|
||||
"process artifacts, and illuminated margins",
|
||||
"brutalist civic planning: concrete signage, zoning blocks, transit diagrams, "
|
||||
"infrastructure maps, and stern public-service clarity",
|
||||
)
|
||||
|
||||
|
||||
def build_change_shape(
|
||||
context: Mapping[str, Any],
|
||||
*,
|
||||
max_commits: int = 10,
|
||||
max_files: int = 10,
|
||||
) -> str:
|
||||
"""Render a compact factual digest of the PR: labels, linked issues, commits, files.
|
||||
|
||||
Mirrors the delivery context the Basic Memory CI capture flow collects
|
||||
(ProjectUpdateContext): the goal is to ground the image in the theme of the
|
||||
WHOLE change — what it touches and why — not just the title/description.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
|
||||
labels = [str(label.get("name", "")) for label in context.get("labels") or []]
|
||||
labels = [label for label in labels if label]
|
||||
if labels:
|
||||
lines.append(f"Labels: {', '.join(labels)}")
|
||||
|
||||
issues = context.get("closingIssuesReferences") or []
|
||||
if issues:
|
||||
lines.append("Linked issues:")
|
||||
for issue in issues:
|
||||
number = issue.get("number")
|
||||
title = str(issue.get("title") or "").strip()
|
||||
lines.append(f"- #{number}: {title}" if title else f"- #{number}")
|
||||
|
||||
commits = context.get("commits") or []
|
||||
subjects = [str(commit.get("messageHeadline") or "").strip() for commit in commits]
|
||||
# Merge commits carry no thematic signal — they're branch bookkeeping.
|
||||
subjects = [
|
||||
subject
|
||||
for subject in subjects
|
||||
if subject and not subject.startswith(("Merge branch ", "Merge pull request "))
|
||||
]
|
||||
if subjects:
|
||||
lines.append(f"Commit subjects ({len(subjects)} total):")
|
||||
lines.extend(f"- {subject}" for subject in subjects[:max_commits])
|
||||
if len(subjects) > max_commits:
|
||||
lines.append(f"- ... and {len(subjects) - max_commits} more")
|
||||
|
||||
files = context.get("files") or []
|
||||
if files:
|
||||
additions = sum(int(item.get("additions") or 0) for item in files)
|
||||
deletions = sum(int(item.get("deletions") or 0) for item in files)
|
||||
lines.append(f"Files changed ({len(files)} total, +{additions}/-{deletions}):")
|
||||
ranked = sorted(
|
||||
files,
|
||||
key=lambda item: int(item.get("additions") or 0) + int(item.get("deletions") or 0),
|
||||
reverse=True,
|
||||
)
|
||||
for item in ranked[:max_files]:
|
||||
path = str(item.get("path") or "")
|
||||
lines.append(f"- {path} (+{item.get('additions') or 0}/-{item.get('deletions') or 0})")
|
||||
if len(files) > max_files:
|
||||
lines.append(f"- ... and {len(files) - max_files} more files")
|
||||
|
||||
return "\n".join(lines) if lines else "(no additional change context available)"
|
||||
|
||||
|
||||
def extract_pr_content(pr_body: str) -> str:
|
||||
"""Return the author's own PR description with all managed bot blocks removed."""
|
||||
content = pr_body
|
||||
for start, end in MANAGED_BLOCKS:
|
||||
content = re.sub(
|
||||
rf"{re.escape(start)}.*?{re.escape(end)}",
|
||||
"",
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return content.strip()
|
||||
|
||||
|
||||
def extract_infographic_theme(pr_body: str) -> str | None:
|
||||
pattern = re.compile(
|
||||
rf"{re.escape(THEME_START)}\s*(.*?)\s*{re.escape(THEME_END)}",
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
match = pattern.search(pr_body)
|
||||
if not match:
|
||||
return None
|
||||
theme = match.group(1).strip()
|
||||
return theme or None
|
||||
|
||||
|
||||
def select_image_theme(
|
||||
*,
|
||||
pr_number: int,
|
||||
pr_title: str,
|
||||
pr_body: str,
|
||||
theme_override: str | None,
|
||||
) -> ThemeSelection:
|
||||
if theme_override:
|
||||
return ThemeSelection(theme=theme_override, source=ThemeSource.CLI)
|
||||
body_theme = extract_infographic_theme(pr_body)
|
||||
if body_theme:
|
||||
return ThemeSelection(theme=body_theme, source=ThemeSource.PR_BODY)
|
||||
# Seed on author-owned PR identity, not the review summary, so the pick is
|
||||
# stable across re-reviews of the same PR.
|
||||
seed = f"{pr_number}\n{pr_title}".encode("utf-8")
|
||||
index = int.from_bytes(hashlib.sha256(seed).digest()[:2], byteorder="big") % len(
|
||||
BM_IMAGE_THEME_POOL
|
||||
)
|
||||
return ThemeSelection(theme=BM_IMAGE_THEME_POOL[index], source=ThemeSource.AUTO)
|
||||
|
||||
|
||||
def _preformatted(value: str) -> str:
|
||||
return f"<pre><code>{html.escape(value, quote=False)}</code></pre>"
|
||||
|
||||
|
||||
def build_infographic_provenance_block(
|
||||
*,
|
||||
pr_number: int,
|
||||
output_path: Path,
|
||||
model: str,
|
||||
size: str,
|
||||
quality: str,
|
||||
theme: str,
|
||||
theme_source: ThemeSource,
|
||||
) -> str:
|
||||
return f"""
|
||||
{PROVENANCE_START}
|
||||
<details>
|
||||
<summary>BM Bossbot image choices</summary>
|
||||
|
||||
- Pull request: `#{pr_number}`
|
||||
- Generated asset: `{output_path.as_posix()}`
|
||||
- Image model: `{model}`
|
||||
- Size: `{size}`
|
||||
- Quality: `{quality}`
|
||||
- Image mode: `editorial-image`
|
||||
- Theme source: `{theme_source.value}`
|
||||
|
||||
Theme / visual direction:
|
||||
{_preformatted(theme)}
|
||||
|
||||
</details>
|
||||
{PROVENANCE_END}
|
||||
""".strip()
|
||||
|
||||
|
||||
def upsert_managed_block(body: str, *, block: str, start: str, end: str) -> str:
|
||||
pattern = re.compile(rf"{re.escape(start)}.*?{re.escape(end)}", flags=re.DOTALL)
|
||||
if pattern.search(body):
|
||||
return pattern.sub(block, body, count=1)
|
||||
if body.strip():
|
||||
return f"{body.rstrip()}\n\n{block}\n"
|
||||
return f"{block}\n"
|
||||
|
||||
|
||||
def build_infographic_prompt(
|
||||
*,
|
||||
pr_number: int,
|
||||
pr_title: str,
|
||||
pr_content: str,
|
||||
change_shape: str,
|
||||
theme: str,
|
||||
theme_source: ThemeSource,
|
||||
) -> str:
|
||||
theme_label = (
|
||||
"Selected BM visual direction"
|
||||
if theme_source == ThemeSource.AUTO
|
||||
else "User-supplied visual direction"
|
||||
)
|
||||
|
||||
return f"""
|
||||
Create a polished landscape WebP editorial image for Basic Memory PR #{pr_number}.
|
||||
|
||||
Your subject is the CONTENT of the pull request — what the change does and why
|
||||
it matters — described in the title, description, and change shape below.
|
||||
Express the theme of the whole change as a visual story.
|
||||
|
||||
This image is decoration for the PR conversation. It is NOT a review artifact:
|
||||
do not depict review verdicts, approval, or process. Never render approval
|
||||
stamps, "APPROVED"/"SUCCESS"/"VERDICT" wording, rubber stamps, wax seals of
|
||||
approval, badges, checkmarks, checklists, status lines, SHA strings, or
|
||||
BM Bossbot itself. If the composition needs text, draw it from the change's
|
||||
subject matter only.
|
||||
|
||||
Pull request title:
|
||||
{pr_title}
|
||||
|
||||
Pull request description:
|
||||
{pr_content}
|
||||
|
||||
Change shape — factual delivery context (labels, linked issues, commit
|
||||
subjects, changed files). Use it to understand what the whole PR touches and
|
||||
let that steer the imagery. It is context, NOT captions: never render file
|
||||
paths, diff stats, issue numbers, or commit subjects verbatim in the image.
|
||||
{change_shape}
|
||||
|
||||
{theme_label}:
|
||||
{theme}
|
||||
|
||||
Treat the visual direction as style inspiration only. Do not let it override
|
||||
facts, readability, source material, or the prohibition on review imagery.
|
||||
|
||||
Use image-first composition: create a scene, movie poster, editorial painting,
|
||||
classic photograph, cover image, symbolic tableau, staged artifact, or another
|
||||
visual moment that expresses the PR intent.
|
||||
|
||||
Make the selected direction shape the subject, lighting, composition, props,
|
||||
environment, and mood. Use one strong focal point. Prefer visual metaphor over
|
||||
explanatory UI.
|
||||
|
||||
Use at most a short title and zero to three short labels if text helps. Any text
|
||||
that appears must be high-contrast, smooth, anti-aliased, and readable.
|
||||
|
||||
Do not render an infographic, dashboard, flowchart, timeline strip, checklist,
|
||||
bullet-list panel, data panel, or dense explanatory diagram.
|
||||
|
||||
Avoid fake screenshots, code blocks, invented claims, copyrighted characters,
|
||||
logos, named fictional universes, direct band logos, album art, celebrity
|
||||
likenesses, or decorations that obscure content.
|
||||
""".strip()
|
||||
|
||||
|
||||
@app.command()
|
||||
def generate(
|
||||
pr_number: Annotated[
|
||||
int,
|
||||
typer.Option("--pr-number", min=1, help="Pull request number."),
|
||||
],
|
||||
pr_context_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--pr-context-file",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help=(
|
||||
"JSON from `gh pr view --json "
|
||||
"title,body,labels,files,commits,closingIssuesReferences`."
|
||||
),
|
||||
),
|
||||
],
|
||||
output: Annotated[Path, typer.Option("--output", help="Output .webp path.")],
|
||||
model: Annotated[str, typer.Option("--model", help="OpenAI image model.")] = DEFAULT_MODEL,
|
||||
size: Annotated[str, typer.Option("--size", help="Image size.")] = DEFAULT_SIZE,
|
||||
quality: Annotated[str, typer.Option("--quality", help="Image quality.")] = DEFAULT_QUALITY,
|
||||
retries: Annotated[int, typer.Option("--retries", min=0, help="Retry attempts.")] = 2,
|
||||
theme: Annotated[
|
||||
str | None,
|
||||
typer.Option("--theme", help="Optional visual theme preference."),
|
||||
] = None,
|
||||
provenance_output: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--provenance-output",
|
||||
dir_okay=False,
|
||||
help="Optional file to write the managed PR-body provenance block.",
|
||||
),
|
||||
] = None,
|
||||
print_prompt: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--print-prompt",
|
||||
"--dry-run",
|
||||
help="Print the generated prompt and exit without calling OpenAI. Alias: --dry-run.",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Generate the canonical PR image from the PR's title, description, and change shape."""
|
||||
context = json.loads(pr_context_file.read_text(encoding="utf-8"))
|
||||
if not isinstance(context, Mapping):
|
||||
raise typer.BadParameter("PR context file must contain a JSON object")
|
||||
pr_title = str(context.get("title") or "")
|
||||
pr_body = str(context.get("body") or "")
|
||||
pr_content = extract_pr_content(pr_body)
|
||||
change_shape = build_change_shape(context)
|
||||
theme_selection = select_image_theme(
|
||||
pr_number=pr_number,
|
||||
pr_title=pr_title,
|
||||
pr_body=pr_body,
|
||||
theme_override=theme,
|
||||
)
|
||||
prompt = build_infographic_prompt(
|
||||
pr_number=pr_number,
|
||||
pr_title=pr_title,
|
||||
pr_content=pr_content,
|
||||
change_shape=change_shape,
|
||||
theme=theme_selection.theme,
|
||||
theme_source=theme_selection.source,
|
||||
)
|
||||
if print_prompt:
|
||||
typer.echo(prompt)
|
||||
raise typer.Exit()
|
||||
|
||||
image_result = generate_image_result(
|
||||
prompt=prompt,
|
||||
output_path=output,
|
||||
model=model,
|
||||
size=size,
|
||||
quality=quality,
|
||||
retries=retries,
|
||||
)
|
||||
output_path = image_result.path
|
||||
if provenance_output:
|
||||
provenance_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
provenance_output.write_text(
|
||||
build_infographic_provenance_block(
|
||||
pr_number=pr_number,
|
||||
output_path=output_path,
|
||||
model=model,
|
||||
size=size,
|
||||
quality=quality,
|
||||
theme=theme_selection.theme,
|
||||
theme_source=theme_selection.source,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
typer.echo(output_path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed and refresh shared pytest-testmon data for Git worktrees."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
TESTMON_FILENAMES = (".testmondata", ".testmondata-shm", ".testmondata-wal")
|
||||
TESTMON_CACHE_ENV = "BM_TESTMON_CACHE_DIR"
|
||||
|
||||
|
||||
class TestmonCacheResult(NamedTuple):
|
||||
status: str
|
||||
source_dir: Path
|
||||
destination_dir: Path
|
||||
copied: tuple[Path, ...]
|
||||
|
||||
|
||||
def _run_git(args: list[str], cwd: Path) -> str:
|
||||
return subprocess.check_output(["git", *args], cwd=cwd, text=True).strip()
|
||||
|
||||
|
||||
def resolve_repo_root(repo_root: Path | None = None) -> Path:
|
||||
if repo_root is not None:
|
||||
return repo_root.expanduser().resolve()
|
||||
|
||||
return Path(_run_git(["rev-parse", "--show-toplevel"], Path.cwd())).resolve()
|
||||
|
||||
|
||||
def resolve_cache_dir(repo_root: Path, cache_dir: Path | None = None) -> Path:
|
||||
if cache_dir is not None:
|
||||
return cache_dir.expanduser().resolve()
|
||||
|
||||
if env_cache_dir := os.environ.get(TESTMON_CACHE_ENV):
|
||||
return Path(env_cache_dir).expanduser().resolve()
|
||||
|
||||
git_common_dir = Path(_run_git(["rev-parse", "--git-common-dir"], repo_root))
|
||||
if not git_common_dir.is_absolute():
|
||||
git_common_dir = repo_root / git_common_dir
|
||||
|
||||
return git_common_dir.resolve() / "testmon-cache" / "main"
|
||||
|
||||
|
||||
def _testmon_datafile(directory: Path) -> Path:
|
||||
return directory / ".testmondata"
|
||||
|
||||
|
||||
def _testmon_files(directory: Path) -> list[Path]:
|
||||
return [
|
||||
directory / filename for filename in TESTMON_FILENAMES if (directory / filename).is_file()
|
||||
]
|
||||
|
||||
|
||||
def _remove_path(path: Path) -> None:
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
elif path.exists():
|
||||
path.unlink()
|
||||
|
||||
|
||||
def _copy_testmon_files(source_dir: Path, destination_dir: Path) -> tuple[Path, ...]:
|
||||
destination_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied: list[Path] = []
|
||||
for source in _testmon_files(source_dir):
|
||||
destination = destination_dir / source.name
|
||||
shutil.copy2(source, destination)
|
||||
copied.append(destination)
|
||||
|
||||
return tuple(copied)
|
||||
|
||||
|
||||
def seed_testmon_data(repo_root: Path, cache_dir: Path) -> TestmonCacheResult:
|
||||
local_datafile = _testmon_datafile(repo_root)
|
||||
shared_datafile = _testmon_datafile(cache_dir)
|
||||
|
||||
if local_datafile.exists():
|
||||
return TestmonCacheResult(
|
||||
status="exists",
|
||||
source_dir=cache_dir,
|
||||
destination_dir=repo_root,
|
||||
copied=(),
|
||||
)
|
||||
|
||||
if not shared_datafile.exists():
|
||||
return TestmonCacheResult(
|
||||
status="missing",
|
||||
source_dir=cache_dir,
|
||||
destination_dir=repo_root,
|
||||
copied=(),
|
||||
)
|
||||
|
||||
# A worktree with sidecars but no main database is stale; replace the set
|
||||
# together so SQLite never sees a mixed local/cache snapshot.
|
||||
for filename in TESTMON_FILENAMES:
|
||||
_remove_path(repo_root / filename)
|
||||
|
||||
copied = _copy_testmon_files(cache_dir, repo_root)
|
||||
return TestmonCacheResult(
|
||||
status="seeded",
|
||||
source_dir=cache_dir,
|
||||
destination_dir=repo_root,
|
||||
copied=copied,
|
||||
)
|
||||
|
||||
|
||||
def refresh_testmon_data(repo_root: Path, cache_dir: Path) -> TestmonCacheResult:
|
||||
local_datafile = _testmon_datafile(repo_root)
|
||||
|
||||
if not local_datafile.exists():
|
||||
raise FileNotFoundError(
|
||||
f"No local pytest-testmon data at {local_datafile}; run tests first."
|
||||
)
|
||||
|
||||
cache_parent = cache_dir.parent
|
||||
cache_parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=f".{cache_dir.name}.", dir=cache_parent))
|
||||
backup_dir = cache_parent / f".{cache_dir.name}.previous-{os.getpid()}"
|
||||
copied: tuple[Path, ...] = ()
|
||||
|
||||
try:
|
||||
copied = _copy_testmon_files(repo_root, temp_dir)
|
||||
|
||||
_remove_path(backup_dir)
|
||||
if cache_dir.exists():
|
||||
cache_dir.rename(backup_dir)
|
||||
|
||||
try:
|
||||
temp_dir.rename(cache_dir)
|
||||
except Exception:
|
||||
if backup_dir.exists() and not cache_dir.exists():
|
||||
backup_dir.rename(cache_dir)
|
||||
raise
|
||||
finally:
|
||||
_remove_path(temp_dir)
|
||||
_remove_path(backup_dir)
|
||||
|
||||
return TestmonCacheResult(
|
||||
status="refreshed",
|
||||
source_dir=repo_root,
|
||||
destination_dir=cache_dir,
|
||||
copied=tuple(cache_dir / path.name for path in copied),
|
||||
)
|
||||
|
||||
|
||||
def _print_seed_result(result: TestmonCacheResult) -> None:
|
||||
if result.status == "seeded":
|
||||
print(f"Seeded pytest-testmon data from {result.source_dir} into {result.destination_dir}")
|
||||
elif result.status == "exists":
|
||||
print(f"Local pytest-testmon data already exists at {result.destination_dir}")
|
||||
elif result.status == "missing":
|
||||
print(
|
||||
f"No shared pytest-testmon baseline at {result.source_dir}; "
|
||||
"run `just testmon-refresh` after a full backend test run to create one."
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unexpected seed result: {result.status}")
|
||||
|
||||
|
||||
def _print_refresh_result(result: TestmonCacheResult) -> None:
|
||||
print(f"Published pytest-testmon data from {result.source_dir} to {result.destination_dir}")
|
||||
|
||||
|
||||
def _print_status(repo_root: Path, cache_dir: Path) -> None:
|
||||
print(f"Repo root: {repo_root}")
|
||||
print(f"Worktree data: {_testmon_datafile(repo_root)}")
|
||||
print(f"Shared cache: {_testmon_datafile(cache_dir)}")
|
||||
print(f"Worktree ready: {_testmon_datafile(repo_root).exists()}")
|
||||
print(f"Cache ready: {_testmon_datafile(cache_dir).exists()}")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--repo-root",
|
||||
type=Path,
|
||||
help="Repository root to operate on (default: git rev-parse --show-toplevel)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cache-dir",
|
||||
type=Path,
|
||||
help=(
|
||||
"Shared testmon cache directory "
|
||||
f"(default: ${TESTMON_CACHE_ENV} or <git-common-dir>/testmon-cache/main)"
|
||||
),
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
subparsers.add_parser("seed", help="Copy shared testmon data into this worktree if missing")
|
||||
subparsers.add_parser("refresh", help="Publish this worktree's testmon data to the cache")
|
||||
subparsers.add_parser("status", help="Show local and shared testmon data paths")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
cache_dir = resolve_cache_dir(repo_root, args.cache_dir)
|
||||
|
||||
if args.command == "seed":
|
||||
_print_seed_result(seed_testmon_data(repo_root=repo_root, cache_dir=cache_dir))
|
||||
return 0
|
||||
|
||||
if args.command == "refresh":
|
||||
_print_refresh_result(refresh_testmon_data(repo_root=repo_root, cache_dir=cache_dir))
|
||||
return 0
|
||||
|
||||
if args.command == "status":
|
||||
_print_status(repo_root=repo_root, cache_dir=cache_dir)
|
||||
return 0
|
||||
|
||||
parser.error(f"Unknown command: {args.command}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -99,7 +99,7 @@ def set_package_version(data: dict[str, Any], version: str) -> None:
|
||||
# Version scopes. The two groups map to the two distribution tracks:
|
||||
# core — the Python package and its MCP registry manifest
|
||||
# packages — the host-native agent artifacts (Claude Code plugin + marketplaces,
|
||||
# Hermes, OpenClaw). These are the "plugin/agent artifacts."
|
||||
# Codex plugin, Hermes, OpenClaw). These are the "plugin/agent artifacts."
|
||||
# `all` writes both. Lockstep releases use `all`; targeted fixes can use one group.
|
||||
SCOPES = ("all", "core", "packages")
|
||||
|
||||
@@ -134,6 +134,11 @@ def _update_packages(version: str, *, dry_run: bool) -> None:
|
||||
lambda data: set_claude_marketplace_version(data, version),
|
||||
dry_run=dry_run,
|
||||
)
|
||||
update_json(
|
||||
"plugins/codex/.codex-plugin/plugin.json",
|
||||
lambda data: set_package_version(data, npm_package_version(version)),
|
||||
dry_run=dry_run,
|
||||
)
|
||||
update_text(
|
||||
"integrations/hermes/plugin.yaml",
|
||||
r"^version:\s*.*$",
|
||||
@@ -174,7 +179,7 @@ def main() -> None:
|
||||
choices=SCOPES,
|
||||
default="all",
|
||||
help="Which artifacts to update: all (default), core (Python + server.json), "
|
||||
"or packages (Claude Code plugin, marketplaces, Hermes, OpenClaw)",
|
||||
"or packages (Claude Code plugin, Codex plugin, marketplaces, Hermes, OpenClaw)",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -27,7 +27,7 @@ REQUIRED_HOOK_SCRIPTS = ("hooks/session-start.sh", "hooks/pre-compact.sh")
|
||||
# project at bootstrap). Each must be a parseable schema note.
|
||||
REQUIRED_SCHEMAS = ("session.md", "decision.md", "task.md")
|
||||
# Skills the plugin ships as namespaced slash commands (/basic-memory:<name>).
|
||||
REQUIRED_SKILLS = ("setup", "remember", "status", "share")
|
||||
REQUIRED_SKILLS = ("bm-setup", "bm-remember", "bm-status", "bm-share")
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict:
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the Basic Memory Codex plugin layout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from validate_skills import parse_frontmatter
|
||||
|
||||
|
||||
REQUIRED_SKILLS = (
|
||||
"bm-setup",
|
||||
"bm-orient",
|
||||
"bm-checkpoint",
|
||||
"bm-decide",
|
||||
"bm-remember",
|
||||
"bm-share",
|
||||
"bm-status",
|
||||
)
|
||||
REQUIRED_SCHEMAS = ("codex-session.md", "decision.md", "task.md")
|
||||
REQUIRED_HOOK_EVENTS = ("SessionStart", "PreCompact")
|
||||
REQUIRED_HOOK_SCRIPTS = (
|
||||
"hooks/session-start.sh",
|
||||
"hooks/session-start.py",
|
||||
"hooks/pre-compact.sh",
|
||||
"hooks/pre-compact.py",
|
||||
)
|
||||
REQUIRED_SKILL_AGENT_FILES = ("agents/openai.yaml", "assets/icon.svg")
|
||||
REQUIRED_INTERFACE_ASSETS = {
|
||||
"composerIcon": "assets/app-icon.png",
|
||||
"logo": "assets/logo.png",
|
||||
}
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
payload = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"Missing JSON file: {path}") from None
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit(f"{path}: invalid JSON: {exc}") from None
|
||||
if not isinstance(payload, dict):
|
||||
raise SystemExit(f"{path}: expected a JSON object")
|
||||
return payload
|
||||
|
||||
|
||||
def require_path(path: Path, label: str) -> None:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"Missing {label}: {path}")
|
||||
|
||||
|
||||
def validate_plugin(plugin_dir: Path) -> None:
|
||||
plugin_dir = plugin_dir.resolve()
|
||||
|
||||
# --- Manifest ---
|
||||
manifest_path = plugin_dir / ".codex-plugin" / "plugin.json"
|
||||
manifest = read_json(manifest_path)
|
||||
if manifest.get("name") != "codex":
|
||||
raise SystemExit(f"{manifest_path}: expected name=codex")
|
||||
if manifest.get("skills") != "./skills/":
|
||||
raise SystemExit(f"{manifest_path}: expected skills=./skills/")
|
||||
if manifest.get("mcpServers") != "./.mcp.json":
|
||||
raise SystemExit(f"{manifest_path}: expected mcpServers=./.mcp.json")
|
||||
interface = manifest.get("interface")
|
||||
if not isinstance(interface, dict):
|
||||
raise SystemExit(f"{manifest_path}: missing interface object")
|
||||
if interface.get("displayName") != "Basic Memory for Codex":
|
||||
raise SystemExit(f"{manifest_path}: unexpected interface.displayName")
|
||||
for field, expected_path in REQUIRED_INTERFACE_ASSETS.items():
|
||||
if interface.get(field) != f"./{expected_path}":
|
||||
raise SystemExit(f"{manifest_path}: expected interface.{field}=./{expected_path}")
|
||||
require_path(plugin_dir / expected_path, f"interface.{field} asset")
|
||||
|
||||
# --- MCP ---
|
||||
mcp = read_json(plugin_dir / ".mcp.json")
|
||||
servers = mcp.get("mcpServers")
|
||||
if not isinstance(servers, dict) or "basic-memory" not in servers:
|
||||
raise SystemExit(".mcp.json: expected mcpServers.basic-memory")
|
||||
basic_memory = servers["basic-memory"]
|
||||
if not isinstance(basic_memory, dict):
|
||||
raise SystemExit(".mcp.json: basic-memory server must be an object")
|
||||
if basic_memory.get("command") not in {"uvx", "basic-memory", "bm"}:
|
||||
raise SystemExit(".mcp.json: basic-memory server uses an unexpected command")
|
||||
|
||||
# --- Hooks ---
|
||||
hooks_json = read_json(plugin_dir / "hooks" / "hooks.json")
|
||||
hooks = hooks_json.get("hooks")
|
||||
if not isinstance(hooks, dict):
|
||||
raise SystemExit("hooks/hooks.json: expected hooks object")
|
||||
for event in REQUIRED_HOOK_EVENTS:
|
||||
if event not in hooks:
|
||||
raise SystemExit(f"hooks/hooks.json: missing {event}")
|
||||
for rel in REQUIRED_HOOK_SCRIPTS:
|
||||
script = plugin_dir / rel
|
||||
require_path(script, "hook script")
|
||||
if not os.access(script, os.X_OK):
|
||||
raise SystemExit(f"Hook script is not executable: {script}")
|
||||
|
||||
# --- Skills ---
|
||||
skills_root = plugin_dir / "skills"
|
||||
require_path(skills_root, "skills directory")
|
||||
present = {path.name for path in skills_root.iterdir() if path.is_dir()}
|
||||
for skill_name in REQUIRED_SKILLS:
|
||||
if skill_name not in present:
|
||||
raise SystemExit(f"Missing required skill: skills/{skill_name}/SKILL.md")
|
||||
for skill_dir in sorted(path for path in skills_root.iterdir() if path.is_dir()):
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
require_path(skill_file, "skill file")
|
||||
frontmatter = parse_frontmatter(skill_file)
|
||||
if frontmatter.get("name") != skill_dir.name:
|
||||
raise SystemExit(f"{skill_file}: name must match directory")
|
||||
if not frontmatter.get("description"):
|
||||
raise SystemExit(f"{skill_file}: missing description")
|
||||
for rel in REQUIRED_SKILL_AGENT_FILES:
|
||||
require_path(skill_dir / rel, f"skill {rel}")
|
||||
|
||||
# --- Schemas ---
|
||||
schemas_root = plugin_dir / "schemas"
|
||||
require_path(schemas_root, "schemas directory")
|
||||
for schema_name in REQUIRED_SCHEMAS:
|
||||
schema_file = schemas_root / schema_name
|
||||
require_path(schema_file, "schema")
|
||||
frontmatter = parse_frontmatter(schema_file)
|
||||
if frontmatter.get("type") != "schema":
|
||||
raise SystemExit(f"{schema_file}: expected type: schema")
|
||||
if not frontmatter.get("entity"):
|
||||
raise SystemExit(f"{schema_file}: missing entity")
|
||||
|
||||
print(f"validated Codex plugin in {plugin_dir}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("plugin_dir", nargs="?", default="plugins/codex")
|
||||
args = parser.parse_args()
|
||||
validate_plugin(Path.cwd() / args.plugin_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,9 +4,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PLAIN_SCALAR_MAPPING_VALUE = re.compile(r":(?:\s|$)")
|
||||
|
||||
|
||||
def strip_matching_quotes(value: str) -> str:
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def validate_plain_scalar(path: Path, line_number: int, key: str, value: str) -> None:
|
||||
"""Catch invalid plain-scalar YAML that Codex rejects while loading skills."""
|
||||
stripped = value.strip()
|
||||
if not stripped or stripped[0] in {'"', "'"} or stripped in {"|", ">", "|-", ">-", "|+", ">+"}:
|
||||
return
|
||||
if PLAIN_SCALAR_MAPPING_VALUE.search(stripped):
|
||||
raise SystemExit(
|
||||
f"{path}:{line_number}: invalid YAML frontmatter for {key!r}: "
|
||||
"unquoted ':' followed by whitespace; quote the value"
|
||||
)
|
||||
|
||||
|
||||
def parse_frontmatter(path: Path) -> dict[str, str]:
|
||||
"""Extract top-level frontmatter keys from a Markdown file.
|
||||
|
||||
@@ -15,14 +37,15 @@ def parse_frontmatter(path: Path) -> dict[str, str]:
|
||||
skipped, so nested blocks (a schema note's `schema:`/`settings:` children) can't
|
||||
overwrite a top-level key like `type` or `entity` via last-write-wins. It does
|
||||
not interpret block scalars or multi-line values; callers rely on single-line
|
||||
top-level fields (name, description, type, entity).
|
||||
top-level fields (name, description, type, entity). Keep the Codex-facing YAML
|
||||
guard here dependency-free so package checks work under bare `python3`.
|
||||
"""
|
||||
lines = path.read_text().splitlines()
|
||||
if not lines or lines[0] != "---":
|
||||
raise SystemExit(f"{path}: missing YAML frontmatter")
|
||||
|
||||
frontmatter: dict[str, str] = {}
|
||||
for line in lines[1:]:
|
||||
for line_number, line in enumerate(lines[1:], start=2):
|
||||
if line == "---":
|
||||
break
|
||||
if line[:1] in (" ", "\t"): # nested key — not a top-level field
|
||||
@@ -30,7 +53,10 @@ def parse_frontmatter(path: Path) -> dict[str, str]:
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
frontmatter[key.strip()] = value.strip().strip('"')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
validate_plain_scalar(path, line_number, key, value)
|
||||
frontmatter[key] = strip_matching_quotes(value)
|
||||
else:
|
||||
raise SystemExit(f"{path}: unclosed YAML frontmatter")
|
||||
|
||||
|
||||