feat(plugins): add codex plugin package (#894)
Signed-off-by: phernandez <paul@basicmachines.co>
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -55,6 +55,7 @@ ENV/
|
||||
claude-output
|
||||
**/.claude/settings.local.json
|
||||
.mcp.json
|
||||
!/plugins/codex/.mcp.json
|
||||
.mcpregistry_*
|
||||
/.testmondata
|
||||
.benchmarks/
|
||||
|
||||
@@ -265,8 +265,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 +278,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
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "codex",
|
||||
"version": "0.1.0+codex.20260604201213",
|
||||
"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 |
@@ -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()
|
||||
@@ -0,0 +1,57 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_codex_plugin_mcp_config_is_tracked_and_not_ignored() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
rel_path = "plugins/codex/.mcp.json"
|
||||
|
||||
ignored = subprocess.run(
|
||||
["git", "check-ignore", "--quiet", rel_path],
|
||||
cwd=repo_root,
|
||||
check=False,
|
||||
)
|
||||
assert ignored.returncode == 1
|
||||
|
||||
tracked = subprocess.run(
|
||||
["git", "ls-files", "--error-unmatch", rel_path],
|
||||
cwd=repo_root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert tracked.returncode == 0, tracked.stderr
|
||||
|
||||
|
||||
def test_codex_plugin_hooks_use_clear_portable_runtime_patterns() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
pre_compact_sh = (repo_root / "plugins/codex/hooks/pre-compact.sh").read_text(encoding="utf-8")
|
||||
pre_compact_py = (repo_root / "plugins/codex/hooks/pre-compact.py").read_text(encoding="utf-8")
|
||||
session_start_sh = (repo_root / "plugins/codex/hooks/session-start.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
session_start_py = (repo_root / "plugins/codex/hooks/session-start.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "python3 <<'PY'" not in pre_compact_sh
|
||||
assert "python3 <<'PY'" not in session_start_sh
|
||||
assert 'uv run --script "$script_dir/pre-compact.py"' in pre_compact_sh
|
||||
assert 'uv run --script "$script_dir/session-start.py"' in session_start_sh
|
||||
assert pre_compact_py.startswith("#!/usr/bin/env -S uv run --script\n")
|
||||
assert session_start_py.startswith("#!/usr/bin/env -S uv run --script\n")
|
||||
assert "from datetime import datetime, timezone" in pre_compact_py
|
||||
assert "datetime.now(timezone.utc)" in pre_compact_py
|
||||
assert 'now.isoformat(timespec="seconds")' in pre_compact_py
|
||||
assert "if r not in codex_rows" not in session_start_py
|
||||
|
||||
|
||||
def test_codex_plugin_docs_explain_global_install_and_repo_mapping() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
readme = (repo_root / "plugins/codex/README.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "## Install" in readme
|
||||
assert 'codex plugin marketplace add "$(git rev-parse --show-toplevel)"' in readme
|
||||
assert "codex plugin add codex@basic-memory-local" in readme
|
||||
assert "Plugin installation is user-level in Codex" in readme
|
||||
assert "Each repository still needs its own `.codex/basic-memory.json`" in readme
|
||||