feat(plugins): add codex plugin package (#894)

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
Paul Hernandez
2026-06-04 23:14:07 -05:00
committed by GitHub
parent 8887267256
commit 5973f9b787
41 changed files with 1558 additions and 2 deletions
+20
View File
@@ -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"
}
]
}
+1
View File
@@ -55,6 +55,7 @@ ENV/
claude-output
**/.claude/settings.local.json
.mcp.json
!/plugins/codex/.mcp.json
.mcpregistry_*
/.testmondata
.benchmarks/
+6 -2
View File
@@ -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
+46
View File
@@ -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"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp"
]
}
}
}
+80
View File
@@ -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.
+93
View File
@@ -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`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+31
View File
@@ -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
}
]
}
]
}
}
+232
View File
@@ -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())
+15
View File
@@ -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
+237
View File
@@ -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())
+15
View File
@@ -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
+19
View File
@@ -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
+47
View File
@@ -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.
+30
View File
@@ -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.
+30
View File
@@ -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

+35
View File
@@ -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

+36
View File
@@ -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

+31
View File
@@ -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

+101
View File
@@ -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

+38
View File
@@ -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

+50
View File
@@ -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

+144
View File
@@ -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()
+57
View File
@@ -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