diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..00932f9 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# SessionStart hook for vibe-memory. +# +# Validates memory/ files at the start of each Claude Code on the web +# session so the agent knows immediately if the append-only logs are +# malformed. Runs only in the remote environment; no-op locally. +# +# No external dependencies — the validator is plain Python 3 stdlib. +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +cd "${CLAUDE_PROJECT_DIR:-.}" + +if [ ! -f scripts/validate.py ]; then + echo "[session-start] scripts/validate.py not found, skipping memory validation" >&2 + exit 0 +fi + +python3 scripts/validate.py diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/.github/workflows/memory-pr-comment.yml b/.github/workflows/memory-pr-comment.yml new file mode 100644 index 0000000..edac43d --- /dev/null +++ b/.github/workflows/memory-pr-comment.yml @@ -0,0 +1,47 @@ +name: memory-pr-comment + +on: + pull_request: + paths: + - 'memory/**' + - '.github/workflows/memory-pr-comment.yml' + - 'scripts/pr_comment.py' + +permissions: + pull-requests: write + contents: read + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Checkout head + uses: actions/checkout@v4 + with: + path: head + + - name: Checkout base + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: base + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Build comment body + id: build + run: | + python3 head/scripts/pr_comment.py base head > comment.md + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: Upsert PR comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: vibe-memory + message: ${{ steps.build.outputs.body }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..5760d4a --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,25 @@ +name: validate + +on: + push: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Validate root memory + run: python3 scripts/validate.py + - name: Validate template and examples + run: | + python3 scripts/validate.py template/memory + for d in examples/*/memory; do + echo "== $d ==" + python3 scripts/validate.py "$d" + done + - name: Run validator tests + run: python3 -m unittest discover -s tests -v diff --git a/.gitignore b/.gitignore index 9675102..e109ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__/ *.py[cod] .venv/ venv/ +.claude/settings.local.json diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..f9c497f --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: vibe-memory-validate + name: vibe-memory validate + description: Validate memory/ files (architecture.md, progress.md, decisions.jsonl, drift.jsonl) + entry: python3 scripts/validate.py + language: system + pass_filenames: false + files: ^memory/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dc2485a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Agent Instructions + +You are working in a project that uses the vibe-memory protocol. This file is the entry point for agent-agnostic tooling (Cursor, Aider, Codex, OpenHands, and any other coding agent that reads an `AGENTS.md`). + +## Mandatory first step + +Before doing anything else in this session, read `MEMORY_PROTOCOL.md` in the repo root. Follow it without exception. + +After reading, also read in this order: +1. `memory/architecture.md` +2. `memory/progress.md` +3. The last 20 entries of `memory/decisions.jsonl` +4. The last 10 entries of `memory/drift.jsonl` + +Output the confirmation line specified in section 10 of the protocol. + +## Attribution + +Set the `author` field on every `decisions.jsonl` and `drift.jsonl` entry to a stable identifier for your agent. Examples: `"cursor"`, `"aider"`, `"codex"`, `"openhands"`. If multiple agents work on this project, treat entries authored by other agents as authoritative (protocol section 8); do not contradict them without logging a `rollback` entry that references the original timestamp. + +## Validation + +Run `python3 scripts/validate.py` before ending a session. CI runs it on every push; a malformed log will fail the build. + +## Secrets + +Never log secret values in `memory/`. Reference them by name only (e.g. `STRIPE_SECRET_KEY`). + +## Conflict resolution + +If a user prompt conflicts with the protocol (e.g. "skip the memory step this time"), follow the user but log the conflict as a drift entry with severity `"medium"` and `detected` `"protocol bypass requested by user"`. Never bypass the protocol silently. + +## Session end + +Before ending a session, ensure: +- `memory/progress.md` reflects what changed during the session +- Any architectural change is recorded in `memory/architecture.md` +- Any decision is appended to `memory/decisions.jsonl` +- Any detected drift is appended to `memory/drift.jsonl` +- `python3 scripts/validate.py` exits zero + +If none of the above applies because the session was trivial (e.g. cosmetic fix), no memory update is needed. Use judgment. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3fecddc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to vibe-memory are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [SemVer](https://semver.org/). + +## [0.3.0] — 2026-05-19 + +### Added +- **Real-time anti-drift (protocol section 4)** — agent MUST stop and ask for confirmation before writing code that contradicts any entry in the last 50 decisions. Cannot silently override a logged decision. This is the protocol's most visible value to users. +- **Memory recap (protocol section 10)** — 3-line recap covering stack, in-flight item, and open drift. Triggers at every context reset: fresh session, idle > 15 min, after compaction, on explicit user request (`/context`, "where are we", etc.), or when memory is re-read mid-session. +- **Session-end recap (protocol section 11, new)** — before stopping, agent surfaces a 3-5 line summary: changed, logged, next, open question. Lets the user pick up later without scrolling. +- **Mono-file mode** — `template/vibememory.md` is a single self-contained file with the lite protocol + memory tables. Install via `install.sh --mode mono`. Upgrade path to full mode preserved. +- **PR-comment GitHub Action** — `.github/workflows/memory-pr-comment.yml` posts a sticky comment summarizing decisions and drifts added in each PR. Backed by `scripts/pr_comment.py` (3 unit tests). +- Drift detection AFTER the change moved to section 4.5 (kept distinct from real-time anti-drift in section 4). + +### Changed +- `install.sh` accepts `--mode mono|full` (default: full) +- README quickstart restructured around mode choice +- Protocol version header: 0.2.0 → 0.3.0 + +## [0.2.0] — 2026-05-19 + +### Added +- `LICENSE` — MIT license file (README already advertised MIT) +- `CLAUDE.md` — Claude Code entry point mirroring `replit.md` +- `lovable.md` — Lovable entry point (positions `mem://` as a cache of `memory/`) +- `AGENTS.md` — generic entry point for agent-agnostic tooling +- `scripts/validate.py` — Python 3 stdlib-only validator for `memory/` files; `--check-freshness DAYS` flag +- `scripts/render.py` — renders JSONL logs into a chronological markdown journal +- `tests/test_validate.py` — 22-test suite covering validator + renderer +- `.github/workflows/validate.yml` — CI running the validator and tests on every push and PR +- `.claude/hooks/session-start.sh` + `.claude/settings.json` — SessionStart hook for Claude Code on the web that runs the validator +- `template/memory/` — blank starter files for new projects +- `schemas/decision.schema.json` + `schemas/drift.schema.json` — JSON schemas formalizing the log entry contract +- `install.sh` — one-line installer for fetching the protocol into an existing project +- `.pre-commit-hooks.yaml` — pre-commit integration so the validator runs before each commit +- `CONTRIBUTING.md` — contribution guide covering protocol, entry-point, and tooling changes +- `examples/` — three worked memory states (web app, CLI, library) showing well-formed entries +- Protocol version header in `MEMORY_PROTOCOL.md` + +### Changed +- `README.md` expanded with a quickstart, validation section, web-hook section, CI/license badges, and a "When is this worth it?" caveat +- Repo reorganized: stub starter files moved to `template/memory/`; root `memory/` now self-describes vibe-memory +- Protocol section 1 split into mandatory tier (architecture + progress) and conditional tier (decisions + drift tails) for trivial sessions +- Protocol section 2 reframed around **structural events** (integration activation, DB migration, new secret/dep, first instance of a new pattern, deployment target change, stack swap) instead of "≥2 files" +- Protocol section 10 confirmation line now has a trivial-session variant +- `scripts/validate.py` gains optional `--check-freshness DAYS` flag (warn-only, soft pressure for stale `progress.md` / `architecture.md`) +- `lovable.md` carves the `mem://` (rules) vs `memory/` (journal) boundary; documents Lovable-specific structural events (Cloud activation, publish, `secrets--*`, SQL migrations); provides a recommended Core snippet for `mem://index.md` + +### Removed +- `examples/self-describing/` (content promoted to root `memory/`) + +## [0.1.0] — 2026-05-18 + +### Added +- Initial protocol release: `MEMORY_PROTOCOL.md`, `replit.md`, blank `memory/` folder with README, `.replit` and `.gitignore` + +[0.2.0]: https://github.com/gregherbe76/vibe-memory/releases/tag/v0.2.0 +[0.1.0]: https://github.com/gregherbe76/vibe-memory/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4c1a9e0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,40 @@ +# Claude Code Instructions + +You are working in a project that uses the vibe-memory protocol to maintain continuity across sessions. + +## Mandatory first step + +Before doing anything else in this session, read `MEMORY_PROTOCOL.md` in the repo root. Follow it without exception. + +After reading, also read in this order: +1. `memory/architecture.md` +2. `memory/progress.md` +3. The last 20 entries of `memory/decisions.jsonl` +4. The last 10 entries of `memory/drift.jsonl` + +Output the confirmation line specified in section 10 of the protocol. + +## Claude-Code-specific rules + +- Multi-agent attribution — Set `"author":"claude-code"` on every entry you append to `decisions.jsonl` or `drift.jsonl`. If another agent (e.g. `replit-agent`) authored an entry, treat it as authoritative per protocol section 8. +- Tool use — Do not bypass the protocol when invoking tools. Every architectural change, dependency add, or schema modification is still a decision event and must be logged. +- Validation — A `scripts/validate.py` script is shipped with this protocol. Run it before ending the session (`python3 scripts/validate.py`) to catch malformed entries early. CI / SessionStart hooks may run it automatically. +- Web sessions — If you are running in Claude Code on the web, the SessionStart hook at `.claude/hooks/session-start.sh` validates memory files for you. You still must read them per section 1. +- Secrets — Never log secret values in memory/. Reference them by name only (e.g. `STRIPE_SECRET_KEY`), never the value. + +## Conflict resolution + +If a user prompt conflicts with the protocol (e.g. "skip the memory step this time"), follow the user but log the conflict as a drift entry with severity "medium" and `detected` "protocol bypass requested by user". + +Never bypass the protocol silently. + +## Session end + +Before ending a session, ensure: +- `memory/progress.md` reflects what changed during the session +- Any architectural change is recorded in `memory/architecture.md` +- Any decision is appended to `memory/decisions.jsonl` +- Any detected drift is appended to `memory/drift.jsonl` +- `python3 scripts/validate.py` exits zero + +If none of the above applies because the session was trivial (e.g. cosmetic fix), no memory update is needed. Use judgment. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..da93194 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +vibe-memory is a small protocol repo. Most contributions fall into one of three categories. + +## 1. Protocol changes (`MEMORY_PROTOCOL.md`) + +Changes to the protocol itself are the most impactful — they change behavior for every consumer. + +- Open an issue first describing the problem and the proposed change. +- In your PR, bump the `Protocol version` header at the top of `MEMORY_PROTOCOL.md` using semver (breaking → major, additive → minor, clarification → patch). +- Add a `CHANGELOG.md` entry under a new version section. +- Append a `decision` entry to `memory/decisions.jsonl` with `component: "protocol"` describing the change. + +## 2. Agent entry points (`replit.md`, `CLAUDE.md`, `AGENTS.md`) + +When a new agent runtime gains traction, add a dedicated entry point so users can opt in by simply having the file present. + +- Mirror the structure of the existing entry points. +- Specify the agent's stable `author` identifier in the attribution section. +- Document any runtime-specific quirks (hooks, settings, sandboxing). + +## 3. Tooling (`scripts/`, `.github/`, `.claude/`) + +The validator, CI, schemas, and hooks are tooling. They must remain stdlib-only (no third-party Python deps) so the install path stays a single `curl | tar`. + +- Add tests in `tests/` for any validator change. Run `python3 -m unittest discover -s tests` locally before pushing. +- Keep `scripts/validate.py` working under Python 3.10+. + +## Conventions + +- One JSON object per line in `*.jsonl`. ISO-8601 timestamps. `author` field required. +- `decisions.jsonl` and `drift.jsonl` are append-only — never edit or delete entries. +- `architecture.md` ≤ 200 lines, `progress.md` ≤ 100 lines. + +## Running the validator + +```sh +python3 scripts/validate.py # validate ./memory +python3 scripts/validate.py path/to/memory +python3 -m unittest discover -s tests +``` + +## Releases + +Tags follow `v..`. The version in `MEMORY_PROTOCOL.md`'s header and the latest `CHANGELOG.md` section must match the tag. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d1e1958 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vibe-memory contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MEMORY_PROTOCOL.md b/MEMORY_PROTOCOL.md index 588fd0d..cc1450c 100644 --- a/MEMORY_PROTOCOL.md +++ b/MEMORY_PROTOCOL.md @@ -1,5 +1,7 @@ # Memory Protocol +Protocol version: 0.3.0 + You are a coding agent working on a long-lived project. Your context window is short. The project is not. This protocol gives you a persistent memory so you do not forget, drift, or rewrite what already exists. You MUST follow this protocol. It is not optional. It overrides default behaviors when in conflict. @@ -10,20 +12,40 @@ At the start of every session, BEFORE writing or modifying any code, you MUST re 1. `memory/architecture.md` — the current state of the system 2. `memory/progress.md` — what is done, what is in flight, what is next + +These two are always required. They are small, current-state files; the cost is negligible. + +For sessions that touch architecture, dependencies, schemas, conventions, or any change spanning more than one file, also read: + 3. The last 20 entries of `memory/decisions.jsonl` — recent architectural decisions 4. The last 10 entries of `memory/drift.jsonl` — recent detected drifts -If any of these files is missing, create it empty with the structure defined in `memory/README.md`. Do not skip this step. +For trivial sessions (typo fix, copy change, isolated CSS tweak, single-line bug fix), you may skip steps 3 and 4. You still emit the section 10 confirmation line in the form that reflects what you read. + +If any of these files is missing, create it empty with the structure defined in `memory/README.md`. Do not skip step 1 or 2. ## 2. Decisions are append-only events -Every time you make or apply an architectural choice that affects more than one file, append one line to `memory/decisions.jsonl`. One JSON object per line. Format: +Log on **structural events**, not on every multi-file change. A structural event is something that changes the shape of the project — what it depends on, how it's deployed, what patterns it follows. Trigger a log entry when any of the following happens: + +- New external integration is activated (payments, auth, AI gateway, analytics, monitoring) +- Database migration: new table, column with semantic meaning, RLS policy, function, index strategy +- New secret added (logged by name only, never value) +- New runtime dependency added (npm, pip, cargo, gem, etc.) — include version +- First instance of a new architectural pattern (first server function, first authenticated route, first background job, first feature flag) +- Deployment target change (host, region, runtime version) +- Stack swap (one framework / ORM / library replaced by another) +- Reversal of a prior decision (use `type: "rollback"` referencing the original timestamp) + +Do **not** log on: a new content page, a new button, a colour change, a typo fix, copy edits, isolated styling tweaks. Touching multiple files alone is not enough; the change must reshape the project's structure or dependencies. + +Format — one JSON object per line: {"timestamp":"ISO-8601","type":"decision","component":"","change":"","reason":"","impact":[""],"author":"agent"} Valid type values: decision, constraint, convention, dependency, rollback. -Never edit existing entries. Never delete them. If a decision is reversed, append a new entry with type "rollback" referencing the original timestamp. +Never edit existing entries. Never delete them. ## 3. Architecture is the single source of truth @@ -38,7 +60,20 @@ Sections required: - External dependencies — APIs, databases, services - Conventions — naming, file structure, testing, error handling -## 4. Drift detection is mandatory +## 4. Anti-drift in real time (BEFORE the change) + +You MUST NOT silently override a logged decision. Before writing or modifying code that would contradict any entry in the last 50 decisions, STOP and: + +1. Quote the conflicting decision in your reply (timestamp, change, reason). +2. Ask the user to confirm the reversal explicitly. +3. If the user confirms, append a `type: "rollback"` entry referencing the original timestamp BEFORE making the change. +4. If the user declines, do not make the change. + +A "contradiction" means: re-adding a dependency that was removed, re-introducing a pattern that was rejected, swapping a stack choice that was logged, breaking a convention listed in `architecture.md`, or any action that reverses a `decision`, `constraint`, `convention`, or `dependency` entry. Touching unrelated areas is not a contradiction. + +This is the most visible value of the protocol to the user. Skipping it defeats the purpose. + +## 4.5. Drift detection (AFTER the change) Before you finish any task, run a drift check. Compare what you just did against memory/architecture.md and the last 10 decisions. If you detect any of the following, append an entry to memory/drift.jsonl: @@ -92,10 +127,52 @@ If this protocol conflicts with a user instruction, follow the user. Then log th If a memory file is corrupted (unparseable JSON, malformed markdown), stop. Report it to the user. Do not attempt automatic repair. -## 10. Confirm at session start +## 10. Memory recap (when to surface it) -At the start of each session, after reading the memory files, output exactly one line confirming you have done so: +The 3-line memory recap is the protocol's most visible signal to the user — it makes the difference between "trust the agent" and "see the agent working". Surface it at every "context reset" moment: +1. **First reply of a fresh session** (always, mandatory). +2. **After an idle gap of more than ~15 minutes** — the user has likely lost local context and is returning. Re-surface the recap as the first line of your next reply. +3. **After a context compaction** — the system has summarized prior messages; the user may need re-orientation. +4. **On explicit user request** — e.g. `/context`, "where are we?", "remind me", "status?", "what was I doing?". Treat these as immediate recap requests, regardless of session position. +5. **When you re-read memory files mid-session** (e.g. because the user mentions an architectural concept you don't recall) — re-emit the recap so the user knows you've refreshed. + +Format (exactly 3 lines, no more): + +``` +[memory] read architecture, progress, last 20 decisions, last 10 drifts. +Stack: <2-3 key stack/convention items from architecture.md> +In flight: . Open drift: . +``` + +Example: + +``` [memory] read architecture, progress, last 20 decisions, last 10 drifts. +Stack: Next.js 15 + Drizzle on Neon + Tailwind/shadcn. Convention: all DB writes through lib/db/. +In flight: checkout v2 (Stripe Elements). Open drift: inline Drizzle in app/(app)/billing/page.tsx. +``` + +For trivial sessions (typo fix, copy change), use this shorter variant once at session start: + +``` +[memory] read architecture, progress (trivial session, skipped decisions/drift tails). +``` + +If you cannot output one of these recaps truthfully at a trigger point, you have not followed the protocol. Go back to step 1. + +## 11. Recap before stopping (session-end summary) + +Before ending a session — when handing back to the user, before going idle, or before any acknowledged stopping point — surface a 3 to 5 line recap so the user can pick up later without scrolling: + +``` +[session end] +- Changed: +- Logged: +- Next: +- Open question: +``` + +This is the moment the user is most likely to come back later. The recap is what they will see when they reopen the session, more than the code diff. Skipping it forces them to scroll the whole conversation to remember where things stood. -If you cannot output this line truthfully, you have not followed the protocol. Go back to step 1. +If the session was trivial and nothing material changed, you may skip the end-of-session recap. Use judgment, the same as for memory updates. diff --git a/README.md b/README.md index b8362a2..f5c32b5 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,145 @@ # vibe-memory -A memory protocol for vibe coding agents. Give your Replit Agent a persistent memory in 30 seconds. +[![validate](https://github.com/gregherbe76/vibe-memory/actions/workflows/validate.yml/badge.svg)](https://github.com/gregherbe76/vibe-memory/actions/workflows/validate.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Protocol version](https://img.shields.io/badge/protocol-v0.3.0-informational)](MEMORY_PROTOCOL.md) + +A memory protocol for vibe coding agents. Persistent memory for coding agents — across sessions, across agents, across months. + +Works with Replit Agent, Claude Code, Lovable, Cursor, Aider, Codex, OpenHands, and any agent that reads instruction files from the repo. ## How it works -The agent reads MEMORY_PROTOCOL.md and replit.md at the start of every session. It logs decisions, detects drift, tracks progress. No CLI, no package, no MCP. Just files. +The agent reads `MEMORY_PROTOCOL.md` and an entry-point file for its runtime (`replit.md`, `CLAUDE.md`, `lovable.md`, or `AGENTS.md`) at the start of every session. It logs decisions, detects drift, tracks progress. No CLI, no package, no MCP. Just files. + +## When is this worth it? + +vibe-memory pays off when **time** and **architectural change** stack up. Use it for: + +- ✅ Projects expected to live more than a month, with multiple sessions +- ✅ Multiple agents (or multiple humans) working on the same project +- ✅ Architecture that evolves: refactors, schema migrations, dependency swaps + +Skip it for: + +- ❌ Weekend prototype or throwaway MVP +- ❌ A 1–2 page static site +- ❌ Anything where the whole project fits in one prompt + +If two weeks in your `memory/` files don't reflect reality, you've over-applied the protocol. Simplify (drop drift logging, keep only `architecture.md`) or fall back to your agent's native memory. The validator's `--check-freshness DAYS` flag warns when `progress.md` / `architecture.md` go stale. + +## Quickstart + +Two modes. Pick one: + +### Mono-file mode (recommended for solo / weekend / MVP) + +A single `vibememory.md` file that contains the lite protocol AND your memory: + +```sh +curl -sSL https://raw.githubusercontent.com/gregherbe76/vibe-memory/main/install.sh | bash -s -- --mode mono +``` + +You get one file to edit. The agent reads it top-to-bottom at session start, appends to the tables at the bottom as it works. No validator, no CI, no JSON. Upgrade to full mode if the project grows. + +### Full mode (multi-agent, multi-runtime, CI-validated) + +The full protocol with separate `architecture.md`, `progress.md`, append-only JSONL logs, validator, schemas, and optional hooks: + +```sh +curl -sSL https://raw.githubusercontent.com/gregherbe76/vibe-memory/main/install.sh | bash +``` + +Or pin to a release: + +```sh +curl -sSL https://raw.githubusercontent.com/gregherbe76/vibe-memory/main/install.sh | bash -s -- --ref v0.3.0 +``` + +The installer drops the protocol files, entry points, validator, a blank `memory/` folder, and the optional Claude Code SessionStart hook. It never overwrites existing files. + +Then start a session — the agent reads `MEMORY_PROTOCOL.md`, follows the rules, and emits the section 10 confirmation recap. + +### Manual install + +```sh +curl -sSL https://github.com/gregherbe76/vibe-memory/archive/refs/heads/main.tar.gz \ + | tar -xz --strip-components=1 \ + vibe-memory-main/MEMORY_PROTOCOL.md \ + vibe-memory-main/replit.md \ + vibe-memory-main/CLAUDE.md \ + vibe-memory-main/AGENTS.md \ + vibe-memory-main/scripts \ + vibe-memory-main/schemas \ + vibe-memory-main/template +mv template/memory ./memory +rmdir template +python3 scripts/validate.py +``` ## Structure -- MEMORY_PROTOCOL.md — the rules the agent follows -- replit.md — Replit-specific entry point -- memory/architecture.md — current state of the system -- memory/progress.md — what's done, in flight, blocked -- memory/decisions.jsonl — append-only decision log -- memory/drift.jsonl — append-only drift log +- `MEMORY_PROTOCOL.md` — the rules the agent follows (versioned, semver) +- `replit.md`, `CLAUDE.md`, `lovable.md`, `AGENTS.md` — runtime-specific entry points +- `memory/` — this repo's own memory; self-describes vibe-memory +- `template/memory/` — blank starter files for new projects (full mode) +- `template/vibememory.md` — single-file starter (mono mode) +- `examples/` — three worked memory states (web app, CLI, library) +- `scripts/validate.py` — Python 3 stdlib validator +- `scripts/render.py` — render `decisions.jsonl` + `drift.jsonl` into a human-readable markdown journal +- `schemas/` — JSON schemas for decision and drift entries +- `tests/` — unittest suite for the validator +- `.claude/` — SessionStart hook + settings for Claude Code on the web +- `.github/workflows/validate.yml` — CI running the validator on every push +- `install.sh` — one-line installer +- `.pre-commit-hooks.yaml` — pre-commit integration + +## Validating + +`scripts/validate.py` checks: + +- `architecture.md` exists and is ≤ 200 lines +- `progress.md` exists and is ≤ 100 lines +- every line in `decisions.jsonl` / `drift.jsonl` is valid JSON with required fields, valid type/severity, and an ISO-8601 timestamp + +Exit code 0 on success, 1 on any issue. + +```sh +python3 scripts/validate.py # validate ./memory +python3 scripts/validate.py path/to/memory # validate a specific dir +python3 scripts/validate.py --check-freshness 30 # warn if progress/architecture stale +python3 -m unittest discover -s tests # run the validator's own tests +``` + +## Reading the journal + +JSONL is the source of truth; if you'd rather read a chronological markdown view, render it: + +```sh +python3 scripts/render.py # to stdout +python3 scripts/render.py --output JOURNAL.md # to a file +``` + +## Pre-commit hook + +Add to your project's `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/gregherbe76/vibe-memory + rev: v0.2.0 + hooks: + - id: vibe-memory-validate +``` + +## Claude Code on the web + +The included `.claude/settings.json` registers a SessionStart hook that runs the validator automatically. The installer drops it into `.claude/` so every web session begins with a green validation check. + +## Multi-agent + +Each entry in `decisions.jsonl` and `drift.jsonl` carries an `author` field. When more than one agent works on a project (e.g. Claude Code reviewing what Cursor wrote), each agent treats the other's entries as authoritative and logs a `rollback` entry if it needs to reverse a prior decision. See `MEMORY_PROTOCOL.md` section 8. ## License -MIT +MIT — see [LICENSE](LICENSE). Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/examples/README.md b/examples/README.md index 44671b3..0c92c7a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,9 @@ # Examples -This folder contains example "filled" memory states to help new users understand what a real project's memory looks like in operation. +Worked memory states for three common project shapes. Look at these when you want to see what well-formed entries look like in context. -## self-describing/ +- `web-app/` — a Next.js + Postgres SaaS midway through building checkout +- `cli/` — a Rust CLI tool with one shipped release and a feature in flight +- `library/` — a Python library with a documented API contract and a deprecation in progress -The memory files as they would be filled if the protocol described itself — i.e., what the agent generates when asked to document the vibe-memory repo as its own project. Useful as a reference for what well-formed memory entries look like. - -These are illustrative only. Your project's actual `memory/` folder should describe YOUR project, not the protocol. +These are illustrative. Your project's actual `memory/` should describe **your** project — copy the blank starter from `template/memory/`, not from here. diff --git a/examples/cli/memory/architecture.md b/examples/cli/memory/architecture.md new file mode 100644 index 0000000..e8e5cd7 --- /dev/null +++ b/examples/cli/memory/architecture.md @@ -0,0 +1,45 @@ +# Architecture + +Last updated: 2026-05-02 +Current version: 1.2.0 + +## Stack + +- Rust 1.78 (edition 2021) +- `clap` 4 for CLI parsing +- `tokio` 1.38 for async runtime (only the HTTP fetch path) +- `reqwest` 0.12 with rustls for HTTPS +- `serde` + `serde_json` for config +- Published as `vendor-watch` on crates.io and as static binaries via GitHub Releases (cargo-dist) + +## Components + +- `src/main.rs` — entry point, sets up `clap` and dispatches to subcommands +- `src/cmd/` — one module per subcommand: `init`, `check`, `watch`, `report` +- `src/source/` — vendor source adapters (npm registry, PyPI, GitHub releases). One file per adapter implementing the `Source` trait. +- `src/store/` — local SQLite cache via `rusqlite`; schema in `src/store/schema.sql` +- `src/notify/` — desktop notification + webhook dispatch +- `tests/` — integration tests using `assert_cmd` + +## Data flow + +1. User runs a subcommand. `clap` parses args, builds a `Config`. +2. `cmd` module loads `~/.config/vendor-watch/config.toml`. +3. `source` adapters fetch current versions; `store` reads cached previous versions. +4. Diffs go to `notify` (stdout, desktop notification, or webhook). +5. `store` writes the new versions back. + +## External dependencies + +- npm registry (`https://registry.npmjs.org`) +- PyPI (`https://pypi.org/pypi`) +- GitHub Releases API (`https://api.github.com`; optional `GITHUB_TOKEN` for rate limit) +- Local SQLite file at `~/.local/share/vendor-watch/cache.db` + +## Conventions + +- One subcommand per file in `src/cmd/`; module name == subcommand name. +- Errors flow through `anyhow::Result` at the binary boundary; library code uses `thiserror`-derived enums. +- All HTTP calls go through `source::http_client()` so retry + timeout policy is one place. +- Tests in `tests/` use `assert_cmd` + `predicates`; no real network calls (use `wiremock`). +- `clippy::pedantic` is enabled in CI; warnings fail the build. diff --git a/examples/cli/memory/decisions.jsonl b/examples/cli/memory/decisions.jsonl new file mode 100644 index 0000000..975180f --- /dev/null +++ b/examples/cli/memory/decisions.jsonl @@ -0,0 +1,4 @@ +{"timestamp":"2026-02-10T12:00:00Z","type":"decision","component":"cli","change":"adopt clap 4 derive macros (vs builder API)","reason":"shorter, easier to read; matches the rest of the Rust ecosystem","impact":["src/main.rs","src/cmd/"],"author":"aider"} +{"timestamp":"2026-04-08T17:25:00Z","type":"decision","component":"storage","change":"migrate cache from JSON file to SQLite via rusqlite","reason":"JSON file rewrites caused data loss when two `watch` runs raced","impact":["src/store/","Cargo.toml"],"author":"aider"} +{"timestamp":"2026-04-22T09:15:00Z","type":"dependency","component":"http","change":"switch reqwest TLS backend from native-tls to rustls","reason":"avoid linking to system OpenSSL; simpler static binaries","impact":["Cargo.toml","src/source/http.rs"],"author":"aider"} +{"timestamp":"2026-04-25T20:00:00Z","type":"decision","component":"release","change":"publish 1.2.0 to crates.io and cut binaries via cargo-dist","reason":"PyPI source landed; users were asking","impact":["Cargo.toml","CHANGELOG.md"],"author":"aider"} diff --git a/examples/cli/memory/drift.jsonl b/examples/cli/memory/drift.jsonl new file mode 100644 index 0000000..6a4cac4 --- /dev/null +++ b/examples/cli/memory/drift.jsonl @@ -0,0 +1 @@ +{"timestamp":"2026-04-30T11:42:00Z","type":"drift","severity":"low","detected":"direct reqwest::get call bypasses source::http_client() retry policy","location":"src/source/github.rs:88","suggested_action":"route through source::http_client()"} diff --git a/examples/cli/memory/progress.md b/examples/cli/memory/progress.md new file mode 100644 index 0000000..408dd31 --- /dev/null +++ b/examples/cli/memory/progress.md @@ -0,0 +1,25 @@ +# Progress + +Last updated: 2026-05-02 + +## In progress + +- Add `report --format html` (started 2026-04-28). Templating done; CSS still placeholder. + +## Next + +1. Cache GitHub Releases responses with ETag to reduce rate-limit pressure +2. Add `watch --interval` polling mode (currently only one-shot) +3. Cut 1.3.0 release once HTML report lands + +## Completed (last 10) + +- 2026-04-25 — Released 1.2.0 to crates.io +- 2026-04-22 — Switched HTTP client TLS backend to rustls +- 2026-04-15 — Added PyPI source adapter +- 2026-04-08 — Migrated cache schema from JSON file to SQLite +- 2026-03-30 — Released 1.1.0 + +## Blocked + +None. diff --git a/examples/library/memory/architecture.md b/examples/library/memory/architecture.md new file mode 100644 index 0000000..2ae8fb9 --- /dev/null +++ b/examples/library/memory/architecture.md @@ -0,0 +1,43 @@ +# Architecture + +Last updated: 2026-05-10 +Current version: 3.1.0 + +## Stack + +- Python 3.10+ (3.10, 3.11, 3.12, 3.13 in CI) +- Pure Python, no compiled extensions +- Build: `hatchling` +- Test: `pytest`, `pytest-asyncio`, `hypothesis` +- Lint: `ruff`, `mypy --strict` +- Distributed on PyPI as `flatcache` + +## Components + +- `flatcache/__init__.py` — public API: `Cache`, `AsyncCache`, `CacheError`, `EvictionPolicy` +- `flatcache/cache.py` — sync `Cache` implementation +- `flatcache/asyncio.py` — async `AsyncCache` implementation; shares storage layer +- `flatcache/storage/` — pluggable backends: `memory`, `file`, `redis` (extra) +- `flatcache/policy/` — eviction policies: LRU, LFU, TTL +- `flatcache/_internal/` — anything under here is unstable; public users must not import it +- `tests/` — mirror of source layout; one test module per source module + +## Data flow + +1. User constructs `Cache(storage=..., policy=...)`. +2. `get`/`set`/`delete` calls go to the policy, which consults storage and decides what to evict. +3. Storage backends implement `Storage` protocol (5 methods). +4. `AsyncCache` is a thin wrapper: same storage layer, async methods. + +## External dependencies + +- `redis` extra: `pip install flatcache[redis]` pulls in `redis>=5` +- No required runtime deps for the core package + +## Conventions + +- Public API surface is **only** what's re-exported in `flatcache/__init__.py`. +- Breaking changes require a major version bump and a deprecation window of one minor version. +- Type hints are mandatory; `mypy --strict` is enforced in CI. +- Tests use `pytest` with parametrization, no xunit-style classes. +- Docstrings follow Google style; rendered via `mkdocs-material`. diff --git a/examples/library/memory/decisions.jsonl b/examples/library/memory/decisions.jsonl new file mode 100644 index 0000000..dfdd197 --- /dev/null +++ b/examples/library/memory/decisions.jsonl @@ -0,0 +1,4 @@ +{"timestamp":"2026-04-12T14:00:00Z","type":"decision","component":"packaging","change":"move redis backend from required dep to `redis` extra","reason":"users without redis were pulling in 8MB of unused deps","impact":["pyproject.toml","flatcache/storage/redis.py","docs/install.md"],"author":"codex"} +{"timestamp":"2026-04-20T10:30:00Z","type":"dependency","component":"build","change":"switch build backend from setuptools to hatchling","reason":"simpler pyproject, faster builds, no setup.py","impact":["pyproject.toml"],"author":"codex"} +{"timestamp":"2026-05-05T09:00:00Z","type":"convention","component":"public-api","change":"deprecate Cache.flush_all() in favor of Cache.clear()","reason":"name was inconsistent with stdlib collections; documented removal in 4.0","impact":["flatcache/cache.py","docs/changelog.md"],"author":"codex"} +{"timestamp":"2026-05-08T18:45:00Z","type":"decision","component":"release","change":"publish 3.1.0 to PyPI","reason":"deprecation warning + Python 3.13 support landed","impact":["pyproject.toml","CHANGELOG.md"],"author":"codex"} diff --git a/examples/library/memory/drift.jsonl b/examples/library/memory/drift.jsonl new file mode 100644 index 0000000..69cead6 --- /dev/null +++ b/examples/library/memory/drift.jsonl @@ -0,0 +1 @@ +{"timestamp":"2026-05-02T16:20:00Z","type":"drift","severity":"high","detected":"flatcache/storage/file.py imports from flatcache._internal in public path; violates `_internal` privacy convention","location":"flatcache/storage/file.py:12","suggested_action":"copy the helper into the storage package or promote it to public API with tests"} diff --git a/examples/library/memory/progress.md b/examples/library/memory/progress.md new file mode 100644 index 0000000..de8c0f6 --- /dev/null +++ b/examples/library/memory/progress.md @@ -0,0 +1,25 @@ +# Progress + +Last updated: 2026-05-10 + +## In progress + +- Deprecating `Cache.flush_all()` in favor of `Cache.clear()` (started 2026-05-05). Warning shipped in 3.1.0; removal planned for 4.0.0. + +## Next + +1. Add `RedisStorage.scan_iter` so large caches can be enumerated without OOM +2. Write migration guide for `flush_all` -> `clear` +3. Investigate user report #218: TTL policy off-by-one at midnight UTC + +## Completed (last 10) + +- 2026-05-08 — Released 3.1.0 +- 2026-04-30 — Added Python 3.13 to CI matrix +- 2026-04-20 — Switched build backend from setuptools to hatchling +- 2026-04-12 — Cut redis backend into a `redis` extra (was a hard dep) +- 2026-03-30 — Replaced custom typing helpers with `typing.Protocol` + +## Blocked + +None. diff --git a/examples/self-describing/architecture.md b/examples/self-describing/architecture.md deleted file mode 100644 index 06df35b..0000000 --- a/examples/self-describing/architecture.md +++ /dev/null @@ -1,39 +0,0 @@ -# Architecture - -Last updated: 2026-05-18 -Current version: 0.1.0 - -## Stack - -No runtime stack. This repository is a documentation-and-convention template (Markdown + JSONL) consumed by a coding agent. No build system, no package manager, no language runtime required. - -## Components - -- `MEMORY_PROTOCOL.md` — the rules the agent follows every session -- `replit.md` — Replit-specific entry point and overrides -- `README.md` — human-facing overview -- `memory/architecture.md` — current state of the system (this file) -- `memory/progress.md` — in-flight, next, blocked -- `memory/decisions.jsonl` — append-only decision log -- `memory/drift.jsonl` — append-only drift log -- `memory/README.md` — structure reference for memory files - -## Data flow - -1. Agent session starts. -2. Agent reads `replit.md` → `MEMORY_PROTOCOL.md` → `memory/architecture.md` → `memory/progress.md` → tail of `decisions.jsonl` → tail of `drift.jsonl`. -3. Agent emits the confirmation line from protocol section 10. -4. Agent performs work; appends decision/drift entries as events occur. -5. At session end, agent updates `architecture.md` and `progress.md` if anything material changed. - -## External dependencies - -None. No network calls, no databases, no third-party services. Files are local to the repo. - -## Conventions - -- `decisions.jsonl` and `drift.jsonl` are append-only. Never edit or delete entries; reverse via a new `rollback` entry. -- `architecture.md` and `progress.md` describe current state and may be overwritten. -- One JSON object per line in `.jsonl` files; ISO-8601 timestamps; `author` field required. -- Secrets referenced by name only, never value. -- Keep `architecture.md` under 200 lines, `progress.md` under 100 lines. diff --git a/examples/self-describing/decisions.jsonl b/examples/self-describing/decisions.jsonl deleted file mode 100644 index 32c682a..0000000 --- a/examples/self-describing/decisions.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"timestamp":"2026-05-18T13:34:27Z","type":"convention","component":"meta","change":"installed vibe-memory protocol","reason":"persistent memory across sessions, drift detection, decision audit trail","impact":["MEMORY_PROTOCOL.md","replit.md","memory/"],"author":"agent"} diff --git a/examples/self-describing/progress.md b/examples/self-describing/progress.md deleted file mode 100644 index a2be73f..0000000 --- a/examples/self-describing/progress.md +++ /dev/null @@ -1,22 +0,0 @@ -# Progress - -Last updated: 2026-05-18 - -## In progress - -Nothing. Import session completed. - -## Next - -1. Define the project goal with the human -2. Decide whether to build an application on top of this template, or keep it as a pure protocol repo -3. Make the first real architectural decision once direction is set - -## Completed (last 10) - -- 2026-05-18 — Import session: explored repo, confirmed no runtime stack, populated architecture.md -- 2026-05-18 — Memory protocol installed - -## Blocked - -None. diff --git a/examples/web-app/memory/architecture.md b/examples/web-app/memory/architecture.md new file mode 100644 index 0000000..bf6fbe4 --- /dev/null +++ b/examples/web-app/memory/architecture.md @@ -0,0 +1,47 @@ +# Architecture + +Last updated: 2026-04-22 +Current version: 0.7.3 + +## Stack + +- TypeScript 5.4, React 19, Next.js 15 (App Router) +- Postgres 16 via Neon (serverless driver) +- Drizzle ORM for schema + queries +- Tailwind CSS 4, shadcn/ui components +- Auth: Clerk +- Payments: Stripe Checkout + webhook handler +- Hosted on Vercel; CI on GitHub Actions + +## Components + +- `app/` — Next.js routes; `(marketing)`, `(app)`, `(api)` route groups +- `app/api/stripe/webhook/route.ts` — Stripe event handler, idempotent on `event.id` +- `lib/db/` — Drizzle schema, migrations, query helpers +- `lib/billing/` — Stripe client wrapper, plan→price mapping +- `lib/auth/` — Clerk wrappers, `requireUser` server helper +- `components/ui/` — shadcn primitives (do not edit, regenerate via CLI) +- `components/app/` — product-specific components + +## Data flow + +1. Request hits a Next.js route. Middleware checks Clerk session. +2. Server component or server action calls helpers in `lib/`. +3. `lib/db/` issues queries via Drizzle → Neon Postgres. +4. Stripe webhooks land at `/api/stripe/webhook`, verify signature, mutate DB inside a transaction. +5. Mutations trigger `revalidatePath` for affected routes. + +## External dependencies + +- Neon Postgres (`DATABASE_URL`) +- Clerk (`CLERK_SECRET_KEY`, `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`) +- Stripe (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`) +- Resend for transactional email (`RESEND_API_KEY`) + +## Conventions + +- Server actions live next to their route, suffixed `.actions.ts`. +- All DB writes go through `lib/db/`; no inline Drizzle in routes. +- Stripe webhook handlers must be idempotent — key off `event.id`. +- Tests: Vitest for units, Playwright for the checkout flow. +- No client components above the route segment unless necessary; default to server components. diff --git a/examples/web-app/memory/decisions.jsonl b/examples/web-app/memory/decisions.jsonl new file mode 100644 index 0000000..8182686 --- /dev/null +++ b/examples/web-app/memory/decisions.jsonl @@ -0,0 +1,5 @@ +{"timestamp":"2026-03-01T10:14:00Z","type":"dependency","component":"orm","change":"adopt Drizzle 0.30","reason":"prisma was too heavy for serverless cold starts on Neon","impact":["package.json","lib/db/"],"author":"claude-code"} +{"timestamp":"2026-03-12T15:02:00Z","type":"convention","component":"server-actions","change":"server actions colocated next to routes as *.actions.ts","reason":"keep mutation logic discoverable from the route that uses it","impact":["app/"],"author":"claude-code"} +{"timestamp":"2026-03-28T09:41:00Z","type":"dependency","component":"testing","change":"upgrade Vitest 1.x -> 2.x","reason":"native esm support, faster watch mode","impact":["package.json","vitest.config.ts"],"author":"claude-code"} +{"timestamp":"2026-04-10T11:20:00Z","type":"dependency","component":"framework","change":"upgrade Next.js 14 -> 15","reason":"caching defaults changed to opt-in; cleaner for our use case","impact":["package.json","app/"],"author":"claude-code"} +{"timestamp":"2026-04-18T16:30:00Z","type":"decision","component":"checkout","change":"replace Stripe Checkout with Stripe Elements","reason":"want in-app branding and 3DS step inside our flow","impact":["app/(app)/billing/","lib/billing/","app/api/stripe/webhook/route.ts"],"author":"claude-code"} diff --git a/examples/web-app/memory/drift.jsonl b/examples/web-app/memory/drift.jsonl new file mode 100644 index 0000000..0723f1c --- /dev/null +++ b/examples/web-app/memory/drift.jsonl @@ -0,0 +1,2 @@ +{"timestamp":"2026-04-12T08:55:00Z","type":"drift","severity":"medium","detected":"inline Drizzle query in app/(app)/billing/page.tsx violates 'all DB writes go through lib/db/'","location":"app/(app)/billing/page.tsx:34","suggested_action":"extract to lib/db/billing.ts"} +{"timestamp":"2026-04-20T13:11:00Z","type":"drift","severity":"low","detected":"new shadcn component edited in place instead of regenerated","location":"components/ui/button.tsx","suggested_action":"regenerate via shadcn CLI and apply changes via wrapper component"} diff --git a/examples/web-app/memory/progress.md b/examples/web-app/memory/progress.md new file mode 100644 index 0000000..75c613d --- /dev/null +++ b/examples/web-app/memory/progress.md @@ -0,0 +1,25 @@ +# Progress + +Last updated: 2026-04-22 + +## In progress + +- Checkout v2: switching from Stripe Checkout to Stripe Elements (started 2026-04-18). PR #142 open. Needs webhook handler updates for `payment_intent.succeeded` path. + +## Next + +1. Backfill `subscriptions.plan_id` for accounts created before 2026-03-01 +2. Add Playwright coverage for the failed-payment retry flow +3. Migrate `lib/billing/` from CommonJS-style exports to named exports + +## Completed (last 10) + +- 2026-04-19 — Resend integration for receipt emails +- 2026-04-15 — Drizzle migration 0042: add `subscriptions.cancel_at_period_end` +- 2026-04-10 — Upgraded to Next.js 15 +- 2026-04-04 — Clerk webhook for user.deleted now soft-deletes app rows +- 2026-03-28 — Switched from Vitest 1.x to 2.x + +## Blocked + +None. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..bc3e9c5 --- /dev/null +++ b/install.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# vibe-memory installer. +# +# Drops the protocol files, entry points, validator, optional Claude Code +# hook, and a blank memory/ folder into the current directory. +# +# Usage: +# curl -sSL https://raw.githubusercontent.com/gregherbe76/vibe-memory/main/install.sh | bash +# curl -sSL https://raw.githubusercontent.com/gregherbe76/vibe-memory/main/install.sh | bash -s -- --ref v0.2.0 +# +# Will not overwrite existing files. Re-run is safe. +set -euo pipefail + +REF="main" +REPO="gregherbe76/vibe-memory" +MODE="full" + +while [[ $# -gt 0 ]]; do + case "$1" in + --ref) REF="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --mode) MODE="$2"; shift 2 ;; + -h|--help) + sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +case "$MODE" in + full|mono) ;; + *) echo "unknown --mode: $MODE (expected: full, mono)" >&2; exit 2 ;; +esac + +TARGET="${PWD}" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +echo "[install] fetching ${REPO}@${REF}" +curl -sSL "https://github.com/${REPO}/archive/${REF}.tar.gz" \ + | tar -xz -C "$TMP" --strip-components=1 + +copy() { + local src="$1" dest="$2" + if [ -e "${TARGET}/${dest}" ]; then + echo "[install] skip ${dest} (already exists)" + return + fi + mkdir -p "$(dirname "${TARGET}/${dest}")" + cp -R "${TMP}/${src}" "${TARGET}/${dest}" + echo "[install] wrote ${dest}" +} + +if [ "$MODE" = "mono" ]; then + copy "template/vibememory.md" "vibememory.md" + echo + echo "[install] done (mono mode)." + echo "[install] edit vibememory.md, then start a session — the agent reads the whole file." + exit 0 +fi + +copy "MEMORY_PROTOCOL.md" "MEMORY_PROTOCOL.md" +copy "replit.md" "replit.md" +copy "CLAUDE.md" "CLAUDE.md" +copy "lovable.md" "lovable.md" +copy "AGENTS.md" "AGENTS.md" +copy "scripts/validate.py" "scripts/validate.py" +copy "scripts/render.py" "scripts/render.py" +copy "schemas" "schemas" +copy "template/memory" "memory" +copy ".claude/hooks/session-start.sh" ".claude/hooks/session-start.sh" +copy ".claude/settings.json" ".claude/settings.json" + +chmod +x "${TARGET}/scripts/validate.py" 2>/dev/null || true +chmod +x "${TARGET}/scripts/render.py" 2>/dev/null || true +chmod +x "${TARGET}/.claude/hooks/session-start.sh" 2>/dev/null || true + +echo +echo "[install] done (full mode)." +echo "[install] next: python3 scripts/validate.py" diff --git a/lovable.md b/lovable.md new file mode 100644 index 0000000..b888ac4 --- /dev/null +++ b/lovable.md @@ -0,0 +1,93 @@ +# Lovable Instructions + +You are working in a Lovable project. This project uses the vibe-memory protocol to maintain continuity across sessions. + +## Mandatory first step + +Before doing anything else in this session, read `MEMORY_PROTOCOL.md` in the repo root. Follow it without exception. + +After reading, also read in this order: +1. `memory/architecture.md` +2. `memory/progress.md` +3. The last 20 entries of `memory/decisions.jsonl` (only if the session touches structure — see protocol section 2) +4. The last 10 entries of `memory/drift.jsonl` (only if the session touches structure) + +Output the confirmation line specified in section 10 of the protocol. + +## Lovable-specific rules + +### Boundary: `mem://` vs `memory/` + +Lovable injects its own native memory (`mem://`) into every prompt. The two systems have different jobs and should not overlap: + +- `mem://` → **rules** applied automatically to every action: design tokens, naming conventions, "never use X", "always use Y", style preferences, code patterns. +- `memory/` → **journal**: what happened, why, when. Structural decisions, dependencies, migrations, drift. + +When in doubt: if it's a preference or constraint, it belongs in `mem://`. If it's an event with a timestamp, it belongs in `memory/`. On conflict, `memory/` is the durable record and wins; re-populate `mem://` from the files when needed, not the other way around. Files in `memory/` are also portable across Replit, Claude Code, Cursor — `mem://` is not. + +### Lean on Lovable's native capabilities + +- **Compression (protocol section 7) — skip on Lovable.** Lovable's `chat_search` already provides retrieval over the full chat history. Do not summarize `decisions.jsonl` proactively. Only compress when the file exceeds the 500-line threshold and the user explicitly asks. +- **Reading the conditional tier — be selective.** Lovable's `` injects relevant files automatically. For UI-only or content-only sessions, you may skip the `decisions.jsonl` / `drift.jsonl` tails (mandatory reads remain `architecture.md` + `progress.md`). +- **Rollback entries vs version history.** Lovable's checkpoint / version history covers code-level reverts. Only log `type: "rollback"` in `decisions.jsonl` when reversing an **architectural** decision (a dependency choice, a pattern decision, a stack swap), not when reverting a code change. + +### Lovable-specific structural events to log + +In addition to the universal triggers in protocol section 2, log these Lovable-specific events: + +- **Integration activation** — Lovable Cloud, Stripe, AI Gateway, any third-party service. This is a major event; include the integration name and what it unlocks. +- **Lovable Cloud activation specifically** — this is the bascule from frontend-only to fullstack. Always log it: `component: "cloud-activation"`, `change: "enabled Lovable Cloud"`, with the resulting capabilities (DB, auth, server functions). +- **Secrets added via `secrets--*`** — Lovable manages secrets outside the codebase (no `.env`). Each new secret is a log entry: name only, never the value. +- **Publication / `presentation-open-publish`** — each publish is a release milestone; log it with the version and what shipped. +- **DB migration via `lovable_sql`** — table, RLS policy, function, or index change. Log with the migration name. + +### Cross-project preferences + +If you maintain `mem://~user/` (user-level cross-project preferences), they take precedence over project rules **only when no project rule applies**. Project-level `mem://` and `memory/` always override `mem://~user/` for that project. + +### Author attribution + +Set `"author":"lovable"` on every entry you append to `decisions.jsonl` or `drift.jsonl`. If another agent (e.g. `claude-code`, `cursor`) authored an entry, treat it as authoritative per protocol section 8. + +### Secrets + +Never log secret values in `memory/`. Reference by name only (e.g. `STRIPE_SECRET_KEY`, `DATABASE_URL`). + +## Validation surface + +The machine-readable contract is enforced, not theoretical. Before writing entries, know that the following will check them: + +- `scripts/validate.py` — checks every line of `decisions.jsonl` and `drift.jsonl` is valid JSON, has required fields, valid `type`/`severity`, ISO-8601 timestamps; checks line caps on `architecture.md` (200) and `progress.md` (100). Exit 0 / 1. +- `schemas/decision.schema.json` + `schemas/drift.schema.json` — JSON Schema Draft 2020-12. Usable by any standard schema validator (ajv, python-jsonschema, etc.) independent of `validate.py`. +- `.pre-commit-hooks.yaml` — exposes `vibe-memory-validate` as a pre-commit hook so malformed entries are rejected at commit time. +- `.github/workflows/validate.yml` — CI runs the validator on every push and PR. +- `.claude/hooks/session-start.sh` — Claude Code on the web runs the validator at session start. +- `tests/test_validate.py` — 22 unit tests on the validator itself. + +If you write an entry that fails any of these checks, you will be caught — by the pre-commit hook locally, by CI in the PR, or by the session-start hook on the next Claude Code session. Write valid entries the first time. + +## Recommended Core snippet for `mem://index.md` + +Lovable's protocol adherence depends on the agent's discipline. To make it harder to skip, set the following at the top of `mem://index.md` (short, imperative, hard to miss): + +``` +RULE: At the start of every session, read MEMORY_PROTOCOL.md and lovable.md. Output the section 10 confirmation line as your first reply. No exceptions. +``` + +Keep this rule alone in its block — surrounding text dilutes it. + +## Conflict resolution + +If a user prompt conflicts with the protocol (e.g. "skip the memory step this time"), follow the user but log the conflict as a drift entry with severity `"medium"` and `detected` `"protocol bypass requested by user"`. + +Never bypass the protocol silently. + +## Session end + +Before ending a session, ensure: +- `memory/progress.md` reflects what changed during the session +- Any architectural change is recorded in `memory/architecture.md` +- Any structural decision (see "Lovable-specific structural events" above) is appended to `memory/decisions.jsonl` +- Any detected drift is appended to `memory/drift.jsonl` + +If the session was trivial (UI tweak, copy change, content addition with no structural impact), no memory update is needed. Use judgment. diff --git a/memory/architecture.md b/memory/architecture.md index d39e40c..b932141 100644 --- a/memory/architecture.md +++ b/memory/architecture.md @@ -1,24 +1,59 @@ # Architecture -Last updated: 2026-05-18 -Current version: 0.1.0 +Last updated: 2026-05-19 +Current version: 0.3.0 ## Stack -To be filled by the agent on first real session. +No runtime stack. This repository is a documentation-and-convention template (Markdown + JSONL) consumed by a coding agent. A single Python 3 stdlib-only validator (`scripts/validate.py`) ships alongside it. No build system, no package manager, no third-party dependencies. ## Components -To be filled by the agent on first real session. +- `MEMORY_PROTOCOL.md` — the rules the agent follows every session; semver header; sections include real-time anti-drift (4), session-start recap (10), session-end recap (11) +- `replit.md` — Replit Agent entry point and overrides +- `CLAUDE.md` — Claude Code entry point and overrides +- `lovable.md` — Lovable entry point and overrides +- `AGENTS.md` — generic entry point for agent-agnostic tooling (Cursor, Aider, Codex, OpenHands) +- `README.md` — human-facing overview, quickstart, badges +- `LICENSE` — MIT +- `CHANGELOG.md` — semver-tracked release notes +- `CONTRIBUTING.md` — how to propose protocol, entry-point, or tooling changes +- `install.sh` — one-line installer (`curl … | bash`) +- `memory/` — this repo's own memory (self-describing) +- `template/memory/` — blank starter files for new projects +- `examples/` — three worked memory states (web app, CLI, library) +- `scripts/validate.py` — Python 3 stdlib validator; supports `validate.py [memory_dir] [--check-freshness DAYS]` +- `scripts/render.py` — renders JSONL logs into a chronological markdown journal (derived view; JSONL stays source of truth) +- `scripts/pr_comment.py` — produces a markdown PR comment diffing memory between two refs +- `template/vibememory.md` — single-file mono-mode starter (lite protocol + memory in one file) +- `.github/workflows/memory-pr-comment.yml` — posts a sticky PR comment summarizing memory changes +- `tests/test_validate.py` — 16-test unittest suite for the validator +- `schemas/decision.schema.json` + `schemas/drift.schema.json` — JSON schemas for log entries +- `.claude/settings.json` + `.claude/hooks/session-start.sh` — SessionStart hook for Claude Code on the web +- `.github/workflows/validate.yml` — CI: runs validator on root, template, and every example, plus the test suite +- `.pre-commit-hooks.yaml` — pre-commit integration for downstream projects ## Data flow -To be filled by the agent on first real session. +1. Agent session starts. +2. (Optional, web sessions) `.claude/hooks/session-start.sh` runs `scripts/validate.py` to confirm memory files are well-formed. +3. Agent reads its entry point (`replit.md`, `CLAUDE.md`, or `AGENTS.md`) → `MEMORY_PROTOCOL.md` → `memory/architecture.md` → `memory/progress.md` → tail of `decisions.jsonl` → tail of `drift.jsonl`. +4. Agent emits the confirmation line from protocol section 10. +5. Agent performs work; appends decision/drift entries as events occur. +6. At session end, agent updates `architecture.md` and `progress.md` if anything material changed; runs `scripts/validate.py`. +7. CI re-validates on every push and PR. ## External dependencies -To be filled by the agent on first real session. +None at runtime. Python 3.10+ (stdlib only) is required to run the validator. GitHub Actions provides CI. No third-party Python packages, no network calls outside install. ## Conventions -To be filled by the agent on first real session. +- `decisions.jsonl` and `drift.jsonl` are append-only. Never edit or delete entries; reverse via a new `rollback` entry. +- `architecture.md` and `progress.md` describe current state and may be overwritten. +- One JSON object per line in `.jsonl` files; ISO-8601 timestamps; `author` field required. +- Secrets referenced by name only, never value. +- Keep `architecture.md` under 200 lines, `progress.md` under 100 lines. +- New projects copy from `template/memory/`, never from the repo's own `memory/`. +- Validator must stay stdlib-only; no third-party Python deps allowed. +- Protocol changes bump the `Protocol version` header and add a `CHANGELOG.md` entry. diff --git a/memory/decisions.jsonl b/memory/decisions.jsonl index 32c682a..62f745a 100644 --- a/memory/decisions.jsonl +++ b/memory/decisions.jsonl @@ -1 +1,33 @@ {"timestamp":"2026-05-18T13:34:27Z","type":"convention","component":"meta","change":"installed vibe-memory protocol","reason":"persistent memory across sessions, drift detection, decision audit trail","impact":["MEMORY_PROTOCOL.md","replit.md","memory/"],"author":"agent"} +{"timestamp":"2026-05-19T00:00:00Z","type":"convention","component":"meta","change":"reorganized repo: stub memory files moved to template/, root memory/ now self-describes vibe-memory","reason":"clearer onboarding — new projects copy from template/, root memory/ demonstrates a real filled state","impact":["memory/","template/memory/"],"author":"claude-code"} +{"timestamp":"2026-05-19T00:00:01Z","type":"dependency","component":"validation","change":"added scripts/validate.py (Python 3 stdlib only)","reason":"detect malformed append-only logs early; runnable in CI or SessionStart hooks","impact":["scripts/validate.py"],"author":"claude-code"} +{"timestamp":"2026-05-19T00:00:02Z","type":"convention","component":"agent-entry","change":"added CLAUDE.md alongside replit.md","reason":"multi-agent support — Claude Code users get the same first-step contract Replit users had","impact":["CLAUDE.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T00:00:03Z","type":"convention","component":"hooks","change":"added .claude/hooks/session-start.sh and .claude/settings.json","reason":"auto-validate memory files at session start in Claude Code on the web","impact":[".claude/hooks/session-start.sh",".claude/settings.json"],"author":"claude-code"} +{"timestamp":"2026-05-19T00:00:04Z","type":"decision","component":"licensing","change":"added MIT LICENSE file","reason":"README advertised MIT license but no LICENSE file was present","impact":["LICENSE"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:00Z","type":"convention","component":"protocol","change":"added Protocol version header to MEMORY_PROTOCOL.md, starting at 0.2.0","reason":"semver tracking so downstream consumers can pin","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:01Z","type":"dependency","component":"validation","change":"refactored scripts/validate.py to accept a memory_dir positional arg","reason":"makes the validator testable and reusable across template/ and examples/","impact":["scripts/validate.py","tests/test_validate.py"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:02Z","type":"dependency","component":"ci","change":"added .github/workflows/validate.yml running validator on root + template + examples + tests","reason":"prove the contract holds on every push","impact":[".github/workflows/validate.yml"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:03Z","type":"dependency","component":"validation","change":"added tests/test_validate.py — 16 unittest cases for the validator","reason":"prevent regressions in validation logic","impact":["tests/test_validate.py","tests/__init__.py"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:04Z","type":"convention","component":"agent-entry","change":"added AGENTS.md as generic entry point","reason":"support agent-agnostic tooling (Cursor, Aider, Codex, OpenHands)","impact":["AGENTS.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:05Z","type":"convention","component":"docs","change":"added CONTRIBUTING.md and CHANGELOG.md","reason":"standardize protocol contribution flow and release tracking","impact":["CONTRIBUTING.md","CHANGELOG.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:06Z","type":"decision","component":"distribution","change":"added install.sh one-line installer with --ref pinning","reason":"reduce install friction below the 'curl | tar' baseline","impact":["install.sh","README.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:07Z","type":"convention","component":"validation","change":"added .pre-commit-hooks.yaml exposing vibe-memory-validate hook","reason":"let downstream projects wire the validator into pre-commit","impact":[".pre-commit-hooks.yaml"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:08Z","type":"convention","component":"contract","change":"added schemas/decision.schema.json and schemas/drift.schema.json","reason":"formalize the log entry contract so other tooling can validate independently","impact":["schemas/decision.schema.json","schemas/drift.schema.json"],"author":"claude-code"} +{"timestamp":"2026-05-19T01:00:09Z","type":"convention","component":"examples","change":"added examples/{web-app,cli,library}/memory worked states","reason":"show new users what well-formed memory looks like in real project shapes","impact":["examples/"],"author":"claude-code"} +{"timestamp":"2026-05-19T02:00:00Z","type":"convention","component":"agent-entry","change":"added lovable.md as Lovable-specific entry point","reason":"Lovable has no official instruction-file name but reads project files as long-term context; lovable.md makes vibe-memory adoptable without changing Lovable's behavior","impact":["lovable.md","install.sh","README.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T03:00:00Z","type":"convention","component":"lovable","change":"lovable.md positions mem:// as a cache of memory/; memory/ wins on conflict","reason":"avoid duplication and lock-in; memory/ is portable across Replit/Claude/Cursor while mem:// is Lovable-proprietary","impact":["lovable.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T03:00:01Z","type":"convention","component":"protocol","change":"split section 1 into mandatory (architecture + progress) and conditional (decisions + drift tails) reading tiers","reason":"reading 4 files on a typo-fix session is wasted tokens; protocol now reflects what was implicit in the 'judgment' clause","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T03:00:02Z","type":"dependency","component":"validation","change":"added --check-freshness DAYS to scripts/validate.py (warn-only)","reason":"soft pressure against the append-only log decay failure mode raised by Lovable in self-assessment","impact":["scripts/validate.py","tests/test_validate.py"],"author":"claude-code"} +{"timestamp":"2026-05-19T03:00:03Z","type":"convention","component":"docs","change":"added 'When is this worth it?' caveat to README","reason":"prevent over-application of the protocol on prototype/weekend projects, which discredits the approach","impact":["README.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T04:00:00Z","type":"convention","component":"protocol","change":"section 2 reframed around structural events (integration, DB migration, new dep, new secret, first-instance patterns, deployment target, stack swap) instead of '>=2 files'","reason":"on platforms where most prompts touch multiple files, the old rule fills decisions.jsonl with noise; structural events is a sharper trigger","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T04:00:01Z","type":"convention","component":"lovable","change":"lovable.md expanded: mem:// (rules) vs memory/ (journal) boundary; section 7 skip; Lovable structural events (Cloud activation, publish, secrets--*, lovable_sql); mem://~user/ precedence; recommended Core snippet","reason":"Lovable's self-assessment surfaced concrete frictions; absorbed into the entry point rather than forking the protocol","impact":["lovable.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T04:00:02Z","type":"dependency","component":"tooling","change":"added scripts/render.py producing a chronological markdown journal from JSONL logs","reason":"keep JSONL as source of truth for machine-readability while offering a human-friendly view; resolves Lovable's 'JSONL too verbose' critique without giving up the contract","impact":["scripts/render.py","tests/test_validate.py","README.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T04:00:03Z","type":"decision","component":"distribution","change":"rejected one-branch-per-runtime split","reason":"portability is the core value prop; per-runtime branches would drift and break multi-agent attribution (section 8). Runtime-specific behavior absorbed via entry-point files instead","impact":["lovable.md","replit.md","CLAUDE.md","AGENTS.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T05:00:00Z","type":"convention","component":"lovable","change":"added 'Validation surface' section to lovable.md","reason":"Lovable's round-3 critique missed that validate.py, schemas, pre-commit hook, and CI already exist; making the surface explicit prevents future missed observations","impact":["lovable.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:00Z","type":"decision","component":"protocol","change":"added real-time anti-drift to MEMORY_PROTOCOL.md section 4 (forbid silent overrides; require quote+confirm before reversing a logged decision)","reason":"this is the most visible value moment of the protocol — agent stopping to confirm a reversal is what makes users feel the memory working","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:01Z","type":"convention","component":"protocol","change":"section 10 confirmation line replaced by a 3-line recap (memory marker + stack/conventions + in-flight + open drift)","reason":"silent memory is invisible; the recap is gratuit and changes the perceived value entirely","impact":["MEMORY_PROTOCOL.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:02Z","type":"convention","component":"protocol","change":"added section 11 — session-end recap (changed/logged/next/open question)","reason":"the user is most likely to return after a stop; the recap is what they will see, more than the diff","impact":["MEMORY_PROTOCOL.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:03Z","type":"decision","component":"distribution","change":"added mono-file mode: template/vibememory.md plus install.sh --mode mono","reason":"reduces the entry cost from 30-min setup to one file edited; upgrade path to full mode preserved for projects that grow","impact":["template/vibememory.md","install.sh","README.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:04Z","type":"dependency","component":"ci","change":"added scripts/pr_comment.py + .github/workflows/memory-pr-comment.yml posting a sticky PR comment summarizing memory changes","reason":"surfaces vibe-memory inside the normal dev workflow (PR review) without requiring users to open JSONL","impact":["scripts/pr_comment.py",".github/workflows/memory-pr-comment.yml","tests/test_validate.py"],"author":"claude-code"} +{"timestamp":"2026-05-19T06:00:05Z","type":"convention","component":"versioning","change":"bumped protocol version 0.2.0 -> 0.3.0","reason":"sections 4, 10, 11 are new protocol-level features (anti-drift, session-start recap, session-end recap)","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md","README.md","memory/architecture.md"],"author":"claude-code"} +{"timestamp":"2026-05-19T07:00:00Z","type":"convention","component":"protocol","change":"section 10 recap triggers expanded beyond 'session start': also fires on idle > 15 min, after context compaction, on explicit user request (/context, 'where are we', 'remind me'), and when memory is re-read mid-session","reason":"recap is most valuable when the user has lost context — that happens at more moments than just a fresh session; explicit triggers prevent ambiguity","impact":["MEMORY_PROTOCOL.md","CHANGELOG.md"],"author":"claude-code"} diff --git a/memory/progress.md b/memory/progress.md index 86b884a..6b2b7b6 100644 --- a/memory/progress.md +++ b/memory/progress.md @@ -1,19 +1,30 @@ # Progress -Last updated: 2026-05-18 +Last updated: 2026-05-19 ## In progress -Nothing yet. First session has not started. +Nothing. v0.2.0 polish session completed. ## Next -1. Define the project goal with the human -2. Set up the initial stack -3. Make the first architectural decision +1. Tag v0.2.0 in git and publish a GitHub release +2. Announce v0.2.0 on whatever channels the maintainer uses +3. Gather user feedback on the install.sh flow ## Completed (last 10) +- 2026-05-19 — v0.3.0: real-time anti-drift (section 4), session-start recap (section 10), session-end recap (section 11), mono-file mode (vibememory.md + install --mode mono), PR-comment GitHub Action +- 2026-05-19 — Lovable round 2: protocol section 2 reframed to structural events; lovable.md expanded with mem://↔memory/ boundary, Lovable events (Cloud, publish, secrets, SQL), Core snippet; added scripts/render.py; rejected per-runtime branch split +- 2026-05-19 — Lovable round 1: lovable.md mem:// positioning, tiered reading in protocol, --check-freshness flag, README caveat +- 2026-05-19 — Added lovable.md entry point +- 2026-05-19 — Added AGENTS.md, CONTRIBUTING.md, CHANGELOG.md, protocol version header +- 2026-05-19 — Added install.sh, .pre-commit-hooks.yaml, JSON schemas, examples/{web-app,cli,library} +- 2026-05-19 — Added GitHub Actions CI workflow, 16 validator tests, README badges +- 2026-05-19 — Refactored validate.py to accept a memory_dir arg +- 2026-05-19 — Added LICENSE, CLAUDE.md, scripts/validate.py, .claude/ SessionStart hook +- 2026-05-19 — Reorganized: stub memory files moved to template/, root memory/ now self-describes +- 2026-05-18 — Import session: explored repo, confirmed no runtime stack - 2026-05-18 — Memory protocol installed ## Blocked diff --git a/schemas/decision.schema.json b/schemas/decision.schema.json new file mode 100644 index 0000000..1aae916 --- /dev/null +++ b/schemas/decision.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/gregherbe76/vibe-memory/schemas/decision.schema.json", + "title": "vibe-memory decision entry", + "description": "One JSON object per line in memory/decisions.jsonl", + "oneOf": [ + { + "type": "object", + "required": ["timestamp", "type", "component", "change", "reason", "impact", "author"], + "additionalProperties": true, + "properties": { + "timestamp": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$" + }, + "type": { + "type": "string", + "enum": ["decision", "constraint", "convention", "dependency", "rollback"] + }, + "component": { "type": "string", "minLength": 1 }, + "change": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 }, + "impact": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "author": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "required": ["timestamp", "type", "range", "summary_file", "count"], + "additionalProperties": true, + "properties": { + "timestamp": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$" + }, + "type": { "const": "archive" }, + "range": { "type": "string", "minLength": 1 }, + "summary_file": { "type": "string", "minLength": 1 }, + "count": { "type": "integer", "minimum": 1 } + } + } + ] +} diff --git a/schemas/drift.schema.json b/schemas/drift.schema.json new file mode 100644 index 0000000..644c274 --- /dev/null +++ b/schemas/drift.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/gregherbe76/vibe-memory/schemas/drift.schema.json", + "title": "vibe-memory drift entry", + "description": "One JSON object per line in memory/drift.jsonl", + "type": "object", + "required": ["timestamp", "type", "severity", "detected", "location", "suggested_action"], + "additionalProperties": true, + "properties": { + "timestamp": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$" + }, + "type": { "const": "drift" }, + "severity": { "enum": ["low", "medium", "high"] }, + "detected": { "type": "string", "minLength": 1 }, + "location": { "type": "string", "minLength": 1 }, + "suggested_action": { "type": "string", "minLength": 1 }, + "author": { "type": "string", "minLength": 1 } + } +} diff --git a/scripts/pr_comment.py b/scripts/pr_comment.py new file mode 100644 index 0000000..e2a0485 --- /dev/null +++ b/scripts/pr_comment.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Produce a markdown summary of memory changes between two refs. + +Compares two snapshots of memory/decisions.jsonl and memory/drift.jsonl +(base vs head) and emits a markdown comment suitable for a PR. + +Usage: + python3 scripts/pr_comment.py BASE_DIR HEAD_DIR + # BASE_DIR and HEAD_DIR each contain a memory/ subdirectory + +Writes to stdout. Exits 0 always. +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def _load_lines(path: Path) -> list[str]: + if not path.exists(): + return [] + return [line for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] + + +def _parse_safe(line: str) -> dict | None: + try: + obj = json.loads(line) + return obj if isinstance(obj, dict) else None + except json.JSONDecodeError: + return None + + +def _new_entries(base_lines: list[str], head_lines: list[str]) -> list[dict]: + base_set = set(base_lines) + return [obj for line in head_lines if line not in base_set and (obj := _parse_safe(line))] + + +def render_comment(base_dir: Path, head_dir: Path) -> str: + base_decisions = _load_lines(base_dir / "memory" / "decisions.jsonl") + head_decisions = _load_lines(head_dir / "memory" / "decisions.jsonl") + base_drifts = _load_lines(base_dir / "memory" / "drift.jsonl") + head_drifts = _load_lines(head_dir / "memory" / "drift.jsonl") + + new_decisions = _new_entries(base_decisions, head_decisions) + new_drifts = _new_entries(base_drifts, head_drifts) + + if not new_decisions and not new_drifts: + return "_No vibe-memory changes in this PR._" + + parts: list[str] = ["## 🧠 vibe-memory changes in this PR", ""] + + if new_decisions: + parts.append(f"**{len(new_decisions)} new decision(s):**") + parts.append("") + for d in new_decisions: + t = d.get("type", "decision") + component = d.get("component", "?") + change = d.get("change", "") + reason = d.get("reason", "") + author = d.get("author", "?") + parts.append(f"- **{t}** [{component}] — {change}") + if reason: + parts.append(f" - _why:_ {reason}") + parts.append(f" - _by:_ `{author}`") + parts.append("") + + if new_drifts: + parts.append(f"**{len(new_drifts)} new drift(s):**") + parts.append("") + for d in new_drifts: + severity = d.get("severity", "?") + detected = d.get("detected", "") + location = d.get("location", "") + action = d.get("suggested_action", "") + parts.append(f"- **{severity}** — {detected}") + if location: + parts.append(f" - _at:_ `{location}`") + if action: + parts.append(f" - _fix:_ {action}") + parts.append("") + + parts.append("_Posted by the vibe-memory PR-comment workflow. Edit decisions/drift entries before merging if needed._") + return "\n".join(parts).rstrip() + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Diff vibe-memory between two snapshots and emit a PR comment.") + parser.add_argument("base_dir", help="Directory containing base ref's memory/") + parser.add_argument("head_dir", help="Directory containing head ref's memory/") + args = parser.parse_args(argv) + + sys.stdout.write(render_comment(Path(args.base_dir), Path(args.head_dir))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/render.py b/scripts/render.py new file mode 100755 index 0000000..87e7860 --- /dev/null +++ b/scripts/render.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Render decisions.jsonl and drift.jsonl into a human-readable markdown journal. + +The JSONL files remain the source of truth. This script produces a derived view +for humans (and agents that prefer reading markdown). Output is written to +stdout by default, or to a path with --output. + +Usage: + python3 scripts/render.py # render ./memory to stdout + python3 scripts/render.py path/to/memory + python3 scripts/render.py --output JOURNAL.md +""" +from __future__ import annotations + +import argparse +import json +import sys +from collections import defaultdict +from pathlib import Path + +DEFAULT_MEM = Path(__file__).resolve().parent.parent / "memory" + + +def _load(path: Path) -> list[dict]: + if not path.exists(): + return [] + out = [] + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + continue + return out + + +def _date(ts: str) -> str: + return ts.split("T", 1)[0] if "T" in ts else ts + + +def render(mem: Path) -> str: + decisions = _load(mem / "decisions.jsonl") + drifts = _load(mem / "drift.jsonl") + + by_date: dict[str, list[tuple[str, dict]]] = defaultdict(list) + for d in decisions: + by_date[_date(d.get("timestamp", ""))].append(("decision", d)) + for d in drifts: + by_date[_date(d.get("timestamp", ""))].append(("drift", d)) + + lines = [ + "# Memory journal", + "", + "_Auto-generated from `memory/decisions.jsonl` and `memory/drift.jsonl`. Do not edit by hand — edit the JSONL files (append-only) and regenerate._", + "", + f"- {len(decisions)} decision(s)", + f"- {len(drifts)} drift(s)", + "", + ] + + for date in sorted(by_date.keys()): + lines.append(f"## {date}") + lines.append("") + for kind, entry in sorted(by_date[date], key=lambda kv: kv[1].get("timestamp", "")): + if kind == "decision": + t = entry.get("type", "decision") + component = entry.get("component", "?") + change = entry.get("change", "") + reason = entry.get("reason", "") + impact = entry.get("impact", []) + author = entry.get("author", "?") + if t == "archive": + lines.append( + f"- **archive** ({entry.get('range', '?')}) → `{entry.get('summary_file', '?')}` ({entry.get('count', '?')} entries)" + ) + else: + lines.append(f"- **{t}** [{component}] — {change}") + if reason: + lines.append(f" - _why:_ {reason}") + if impact: + impacts = ", ".join(f"`{p}`" for p in impact) + lines.append(f" - _impact:_ {impacts}") + lines.append(f" - _author:_ {author}") + else: + severity = entry.get("severity", "?") + detected = entry.get("detected", "") + location = entry.get("location", "") + action = entry.get("suggested_action", "") + lines.append(f"- **drift** ({severity}) — {detected}") + if location: + lines.append(f" - _at:_ `{location}`") + if action: + lines.append(f" - _fix:_ {action}") + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Render vibe-memory JSONL logs to markdown.") + parser.add_argument( + "memory_dir", + nargs="?", + default=str(DEFAULT_MEM), + help="Path to memory/ directory (default: ./memory next to this script)", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Write to this path instead of stdout", + ) + args = parser.parse_args(argv) + + out = render(Path(args.memory_dir)) + if args.output: + Path(args.output).write_text(out, encoding="utf-8") + print(f"[render] wrote {args.output} ({len(out.splitlines())} lines)", file=sys.stderr) + else: + sys.stdout.write(out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100755 index 0000000..c4425d5 --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Validate vibe-memory files. + +Checks: +- memory/architecture.md exists and is under 200 lines +- memory/progress.md exists and is under 100 lines +- memory/decisions.jsonl: each line is JSON, has required fields, valid type, ISO-8601 timestamp +- memory/drift.jsonl: each line is JSON, has required fields, valid severity + +Exit code: 0 on success, 1 on any failure. Prints a summary either way. +""" +from __future__ import annotations + +import argparse +import datetime +import json +import re +import sys +from pathlib import Path + +DEFAULT_MEM = Path(__file__).resolve().parent.parent / "memory" +LAST_UPDATED_RE = re.compile(r"^Last updated:\s*(\d{4}-\d{2}-\d{2})", re.MULTILINE) + +ISO8601 = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$" +) + +DECISION_TYPES = {"decision", "constraint", "convention", "dependency", "rollback", "archive"} +DRIFT_SEVERITY = {"low", "medium", "high"} + +DECISION_REQUIRED = {"timestamp", "type", "component", "change", "reason", "impact", "author"} +DRIFT_REQUIRED = {"timestamp", "type", "severity", "detected", "location", "suggested_action"} +ARCHIVE_REQUIRED = {"timestamp", "type", "range", "summary_file", "count"} + + +def fail(errors: list[str], msg: str) -> None: + errors.append(msg) + + +def _rel(path: Path, base: Path) -> str: + try: + return str(path.relative_to(base)) + except ValueError: + return str(path) + + +def check_markdown(path: Path, max_lines: int, errors: list[str], base: Path) -> None: + if not path.exists(): + fail(errors, f"{_rel(path, base)}: missing") + return + lines = path.read_text(encoding="utf-8").splitlines() + if len(lines) > max_lines: + fail(errors, f"{_rel(path, base)}: {len(lines)} lines exceeds {max_lines}-line limit") + + +def check_decisions(path: Path, errors: list[str], base: Path) -> int: + if not path.exists(): + fail(errors, f"{_rel(path, base)}: missing") + return 0 + count = 0 + for i, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not raw.strip(): + continue + count += 1 + try: + obj = json.loads(raw) + except json.JSONDecodeError as e: + fail(errors, f"{_rel(path, base)}:{i}: invalid JSON ({e.msg})") + continue + if not isinstance(obj, dict): + fail(errors, f"{_rel(path, base)}:{i}: must be JSON object") + continue + t = obj.get("type") + if t not in DECISION_TYPES: + fail(errors, f"{_rel(path, base)}:{i}: type {t!r} not in {sorted(DECISION_TYPES)}") + required = ARCHIVE_REQUIRED if t == "archive" else DECISION_REQUIRED + missing = required - obj.keys() + if missing: + fail(errors, f"{_rel(path, base)}:{i}: missing fields {sorted(missing)}") + ts = obj.get("timestamp", "") + if not ISO8601.match(ts): + fail(errors, f"{_rel(path, base)}:{i}: timestamp {ts!r} is not ISO-8601") + if t != "archive" and "impact" in obj and not isinstance(obj["impact"], list): + fail(errors, f"{_rel(path, base)}:{i}: impact must be a list") + return count + + +def check_drift(path: Path, errors: list[str], base: Path) -> int: + if not path.exists(): + fail(errors, f"{_rel(path, base)}: missing") + return 0 + count = 0 + for i, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not raw.strip(): + continue + count += 1 + try: + obj = json.loads(raw) + except json.JSONDecodeError as e: + fail(errors, f"{_rel(path, base)}:{i}: invalid JSON ({e.msg})") + continue + if not isinstance(obj, dict): + fail(errors, f"{_rel(path, base)}:{i}: must be JSON object") + continue + missing = DRIFT_REQUIRED - obj.keys() + if missing: + fail(errors, f"{_rel(path, base)}:{i}: missing fields {sorted(missing)}") + if obj.get("type") != "drift": + fail(errors, f"{_rel(path, base)}:{i}: type must be 'drift'") + sev = obj.get("severity") + if sev not in DRIFT_SEVERITY: + fail(errors, f"{_rel(path, base)}:{i}: severity {sev!r} not in {sorted(DRIFT_SEVERITY)}") + ts = obj.get("timestamp", "") + if not ISO8601.match(ts): + fail(errors, f"{_rel(path, base)}:{i}: timestamp {ts!r} is not ISO-8601") + return count + + +def check_freshness( + mem: Path, + max_days: int, + warnings: list[str], + base: Path, + today: datetime.date | None = None, +) -> None: + today = today or datetime.date.today() + for name in ("progress.md", "architecture.md"): + path = mem / name + if not path.exists(): + continue + m = LAST_UPDATED_RE.search(path.read_text(encoding="utf-8")) + if not m: + warnings.append(f"{_rel(path, base)}: no 'Last updated: YYYY-MM-DD' line") + continue + try: + last = datetime.date.fromisoformat(m.group(1)) + except ValueError: + warnings.append(f"{_rel(path, base)}: 'Last updated' is not a valid date") + continue + age = (today - last).days + if age > max_days: + warnings.append( + f"{_rel(path, base)}: 'Last updated' is {age} days old (> {max_days})" + ) + + +def validate( + mem: Path, + check_freshness_days: int | None = None, + today: datetime.date | None = None, +) -> tuple[int, list[str], list[str], int, int]: + """Validate a memory/ directory. + + Returns (exit_code, errors, warnings, decisions, drifts). + Warnings do not affect exit_code; they are soft pressure for human review. + """ + base = mem.parent + errors: list[str] = [] + warnings: list[str] = [] + check_markdown(mem / "architecture.md", 200, errors, base) + check_markdown(mem / "progress.md", 100, errors, base) + decisions = check_decisions(mem / "decisions.jsonl", errors, base) + drifts = check_drift(mem / "drift.jsonl", errors, base) + if check_freshness_days is not None: + check_freshness(mem, check_freshness_days, warnings, base, today=today) + return (1 if errors else 0, errors, warnings, decisions, drifts) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Validate vibe-memory files.") + parser.add_argument( + "memory_dir", + nargs="?", + default=str(DEFAULT_MEM), + help="Path to memory/ directory (default: ./memory next to this script)", + ) + parser.add_argument( + "--check-freshness", + type=int, + metavar="DAYS", + default=None, + help="Warn (do not fail) if progress.md or architecture.md 'Last updated' is older than DAYS days", + ) + args = parser.parse_args(argv) + + code, errors, warnings, decisions, drifts = validate( + Path(args.memory_dir), + check_freshness_days=args.check_freshness, + ) + if code != 0: + print(f"[validate] FAIL ({len(errors)} issue(s))") + for e in errors: + print(f" - {e}") + else: + print(f"[validate] OK — {decisions} decision(s), {drifts} drift(s)") + if warnings: + print(f"[validate] {len(warnings)} warning(s):") + for w in warnings: + print(f" - {w}") + return code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/template/README.md b/template/README.md new file mode 100644 index 0000000..6e304bf --- /dev/null +++ b/template/README.md @@ -0,0 +1,5 @@ +# Template starter + +This folder contains the empty starter `memory/` files for a new project. Copy them into your project's root and let your agent fill them in. + +See the top-level [README.md](../README.md) for the install snippet. diff --git a/template/memory/README.md b/template/memory/README.md new file mode 100644 index 0000000..622dcfb --- /dev/null +++ b/template/memory/README.md @@ -0,0 +1,31 @@ +# Memory + +This folder is your agent's persistent memory. It survives across sessions, agents, and team members. The agent reads these files at the start of every session and writes to them as it works. + +You can read these files. You can edit them. You can commit them to Git. They are plain text. + +## What's here + +- architecture.md — Living document describing the system as it exists right now. The agent overwrites it when structure changes. +- progress.md — Current operational state: in progress, done, next, blocked. +- decisions.jsonl — Append-only log of architectural decisions. One JSON object per line. Never edited, never deleted. +- drift.jsonl — Append-only log of detected drifts: code that diverges from logged decisions. + +## How to read this folder when you come back + +1. Open progress.md. You know where things stand in 30 seconds. +2. Open architecture.md if you forgot the structure. +3. Tail decisions.jsonl (last 10-20 lines) for recent moves. +4. Open drift.jsonl if something feels off. + +## What you should NOT do + +- Don't delete entries from .jsonl files. They are append-only by design. +- Don't edit old entries to "fix" them. Append a new entry instead. +- Don't store secrets here. + +## What you SHOULD do + +- Commit this folder to Git. Memory is more valuable when versioned. +- Review drift.jsonl weekly. Drift left unfixed compounds. +- Edit architecture.md or progress.md directly when needed. diff --git a/template/memory/architecture.md b/template/memory/architecture.md new file mode 100644 index 0000000..48440ea --- /dev/null +++ b/template/memory/architecture.md @@ -0,0 +1,24 @@ +# Architecture + +Last updated: YYYY-MM-DD +Current version: 0.1.0 + +## Stack + +To be filled by the agent on first real session. + +## Components + +To be filled by the agent on first real session. + +## Data flow + +To be filled by the agent on first real session. + +## External dependencies + +To be filled by the agent on first real session. + +## Conventions + +To be filled by the agent on first real session. diff --git a/template/memory/decisions.jsonl b/template/memory/decisions.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/template/memory/drift.jsonl b/template/memory/drift.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/template/memory/progress.md b/template/memory/progress.md new file mode 100644 index 0000000..903566f --- /dev/null +++ b/template/memory/progress.md @@ -0,0 +1,21 @@ +# Progress + +Last updated: YYYY-MM-DD + +## In progress + +Nothing yet. First session has not started. + +## Next + +1. Define the project goal with the human +2. Set up the initial stack +3. Make the first architectural decision + +## Completed (last 10) + +- YYYY-MM-DD — Memory protocol installed + +## Blocked + +None. diff --git a/template/vibememory.md b/template/vibememory.md new file mode 100644 index 0000000..b7bd3e1 --- /dev/null +++ b/template/vibememory.md @@ -0,0 +1,69 @@ +# vibememory + +Single-file memory for your coding agent. Read top to bottom at session start. Append to the bottom sections as you work. Never edit history above the **Decisions** table. + +For multi-agent / multi-runtime / CI-validated setups, use the full vibe-memory protocol (`MEMORY_PROTOCOL.md` + `memory/`) instead. + +--- + +## Protocol (lite) + +1. **Read this entire file at session start.** Don't write code until you have. +2. **Real-time anti-drift.** If a code change would contradict any row in the Decisions table below, STOP, quote the row, and ask the user to confirm the reversal before proceeding. +3. **Log structural events** in the Decisions table: new integration, DB migration, new secret, new dependency, first instance of a new pattern, deployment target change, stack swap, reversal. Skip: content pages, buttons, colour changes, typos. +4. **Log drift** in the Drift table when you notice code that contradicts an architectural rule or convention. You do not need to fix it in the same session — logging is the priority. +5. **First reply must be a 3-line recap** of architecture, current focus, and any open drift (see protocol section 10 of the full version). +6. **End every non-trivial session with a 3 to 5 line recap**: what changed, what was logged, what's next. +7. **Append-only.** The Decisions and Drift tables are append-only. To reverse a decision, add a new row with `type: rollback` referencing the original date. +8. **Secrets**: never write secret values here. Reference them by name only. + +--- + +## Architecture + +_Overwrite this section whenever structure changes. Keep it concise._ + +Last updated: YYYY-MM-DD + +**Stack:** _(languages, frameworks, runtimes, key libraries)_ + +**Components:** _(top-level modules and their responsibilities)_ + +**Data flow:** _(how requests / events / data move through the system)_ + +**External dependencies:** _(APIs, databases, services)_ + +**Conventions:** _(naming, file structure, testing, error handling, hard rules)_ + +--- + +## Progress + +_Overwrite this section as work moves. Keep it under ~30 lines._ + +Last updated: YYYY-MM-DD + +**In progress:** _(what's being worked on, with date started)_ + +**Next:** _(top 3 items, ordered)_ + +**Completed (last 5):** _(date — what)_ + +**Blocked:** _(reason + what would unblock, or "None")_ + +--- + +## Decisions (append-only, newest at the bottom) + +| Date | Type | Component | Change | Why | Author | +|---|---|---|---|---|---| +| YYYY-MM-DD | convention | meta | adopted vibememory mono-file mode | single-file simpler for solo / weekend projects | you | + +Valid `type` values: decision, constraint, convention, dependency, rollback. + +--- + +## Drift (append-only, newest at the bottom) + +| Date | Severity | Detected | Location | Suggested action | +|---|---|---|---|---| diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 0000000..18cba63 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,249 @@ +"""Tests for scripts/validate.py. + +Run with: python3 -m unittest tests.test_validate +""" +from __future__ import annotations + +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "scripts")) + +import validate # noqa: E402 + +VALID_DECISION = ( + '{"timestamp":"2026-05-19T00:00:00Z","type":"decision","component":"x",' + '"change":"y","reason":"z","impact":["a"],"author":"test"}' +) +VALID_DRIFT = ( + '{"timestamp":"2026-05-19T00:00:00Z","type":"drift","severity":"low",' + '"detected":"x","location":"a:1","suggested_action":"fix"}' +) + + +def write_memory( + tmp: Path, + *, + arch: str | None = "# arch\n", + progress: str | None = "# progress\n", + decisions: str | None = "", + drift: str | None = "", +) -> Path: + mem = tmp / "memory" + mem.mkdir() + if arch is not None: + (mem / "architecture.md").write_text(arch) + if progress is not None: + (mem / "progress.md").write_text(progress) + if decisions is not None: + (mem / "decisions.jsonl").write_text(decisions) + if drift is not None: + (mem / "drift.jsonl").write_text(drift) + return mem + + +class ValidateTests(unittest.TestCase): + def _run(self, **kw): + check_freshness_days = kw.pop("check_freshness_days", None) + today = kw.pop("today", None) + with TemporaryDirectory() as d: + mem = write_memory(Path(d), **kw) + return validate.validate( + mem, + check_freshness_days=check_freshness_days, + today=today, + ) + + def test_empty_memory_is_valid(self): + code, errors, warnings, decisions, drifts = self._run() + self.assertEqual(code, 0, errors) + self.assertEqual((decisions, drifts), (0, 0)) + self.assertEqual(warnings, []) + + def test_one_good_decision_and_drift(self): + code, errors, _w, d, dr = self._run(decisions=VALID_DECISION + "\n", drift=VALID_DRIFT + "\n") + self.assertEqual(code, 0, errors) + self.assertEqual((d, dr), (1, 1)) + + def test_missing_architecture(self): + code, errors, *_ = self._run(arch=None) + self.assertEqual(code, 1) + self.assertTrue(any("architecture.md" in e and "missing" in e for e in errors)) + + def test_missing_progress(self): + code, errors, *_ = self._run(progress=None) + self.assertEqual(code, 1) + self.assertTrue(any("progress.md" in e and "missing" in e for e in errors)) + + def test_architecture_too_long(self): + code, errors, *_ = self._run(arch="x\n" * 201) + self.assertEqual(code, 1) + self.assertTrue(any("exceeds 200" in e for e in errors)) + + def test_progress_too_long(self): + code, errors, *_ = self._run(progress="x\n" * 101) + self.assertEqual(code, 1) + self.assertTrue(any("exceeds 100" in e for e in errors)) + + def test_invalid_json_in_decisions(self): + code, errors, *_ = self._run(decisions="{not json}\n") + self.assertEqual(code, 1) + self.assertTrue(any("invalid JSON" in e for e in errors)) + + def test_bad_decision_type(self): + bad = VALID_DECISION.replace('"type":"decision"', '"type":"nonsense"') + code, errors, *_ = self._run(decisions=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("type 'nonsense'" in e for e in errors)) + + def test_missing_decision_field(self): + bad = VALID_DECISION.replace(',"author":"test"', "") + code, errors, *_ = self._run(decisions=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("missing fields" in e and "author" in e for e in errors)) + + def test_bad_decision_timestamp(self): + bad = VALID_DECISION.replace("2026-05-19T00:00:00Z", "yesterday") + code, errors, *_ = self._run(decisions=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("not ISO-8601" in e for e in errors)) + + def test_decision_impact_must_be_list(self): + bad = VALID_DECISION.replace('"impact":["a"]', '"impact":"a"') + code, errors, *_ = self._run(decisions=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("impact must be a list" in e for e in errors)) + + def test_archive_entry_has_different_required_fields(self): + archive = ( + '{"timestamp":"2026-05-19T00:00:00Z","type":"archive",' + '"range":"2026-01..2026-04","summary_file":"decisions-archive-2026-05.md","count":200}' + ) + code, errors, _w, d, _ = self._run(decisions=archive + "\n") + self.assertEqual(code, 0, errors) + self.assertEqual(d, 1) + + def test_drift_bad_severity(self): + bad = VALID_DRIFT.replace('"severity":"low"', '"severity":"catastrophic"') + code, errors, *_ = self._run(drift=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("severity 'catastrophic'" in e for e in errors)) + + def test_drift_wrong_type(self): + bad = VALID_DRIFT.replace('"type":"drift"', '"type":"decision"') + code, errors, *_ = self._run(drift=bad + "\n") + self.assertEqual(code, 1) + self.assertTrue(any("type must be 'drift'" in e for e in errors)) + + def test_blank_lines_are_ignored(self): + code, errors, _w, d, _ = self._run(decisions=f"\n{VALID_DECISION}\n\n") + self.assertEqual(code, 0, errors) + self.assertEqual(d, 1) + + def test_freshness_off_by_default(self): + # Stale "Last updated" should not produce a warning when check_freshness_days is None. + stale = "# progress\nLast updated: 2020-01-01\n" + _, _, warnings, *_ = self._run(progress=stale) + self.assertEqual(warnings, []) + + def test_freshness_warns_when_stale(self): + stale_arch = "# arch\nLast updated: 2026-01-01\n" + stale_prog = "# progress\nLast updated: 2026-01-01\n" + import datetime as _dt + code, errors, warnings, *_ = self._run( + arch=stale_arch, + progress=stale_prog, + check_freshness_days=30, + today=_dt.date(2026, 5, 19), + ) + self.assertEqual(code, 0, errors) + self.assertEqual(len(warnings), 2) + self.assertTrue(all("Last updated" in w for w in warnings)) + + def test_freshness_ok_when_recent(self): + import datetime as _dt + recent_arch = "# arch\nLast updated: 2026-05-10\n" + recent_prog = "# progress\nLast updated: 2026-05-10\n" + _, _, warnings, *_ = self._run( + arch=recent_arch, + progress=recent_prog, + check_freshness_days=30, + today=_dt.date(2026, 5, 19), + ) + self.assertEqual(warnings, []) + + def test_freshness_missing_last_updated_warns(self): + _, _, warnings, *_ = self._run( + progress="# progress\n(no date line)\n", + check_freshness_days=30, + ) + self.assertTrue(any("no 'Last updated" in w for w in warnings)) + + def test_cli_help(self): + with self.assertRaises(SystemExit) as cm: + validate.main(["--help"]) + self.assertEqual(cm.exception.code, 0) + + +class PrCommentTests(unittest.TestCase): + def _setup(self, tmp: Path, base_dec="", head_dec="", base_drift="", head_drift=""): + for sub, dec, dr in (("base", base_dec, base_drift), ("head", head_dec, head_drift)): + (tmp / sub / "memory").mkdir(parents=True) + (tmp / sub / "memory" / "decisions.jsonl").write_text(dec) + (tmp / sub / "memory" / "drift.jsonl").write_text(dr) + return tmp / "base", tmp / "head" + + def test_no_changes(self): + import pr_comment # noqa: WPS433 + with TemporaryDirectory() as d: + base, head = self._setup(Path(d)) + out = pr_comment.render_comment(base, head) + self.assertIn("No vibe-memory changes", out) + + def test_new_decision_is_reported(self): + import pr_comment + with TemporaryDirectory() as d: + base, head = self._setup(Path(d), head_dec=VALID_DECISION + "\n") + out = pr_comment.render_comment(base, head) + self.assertIn("1 new decision", out) + self.assertIn("**decision**", out) + + def test_new_drift_is_reported(self): + import pr_comment + with TemporaryDirectory() as d: + base, head = self._setup(Path(d), head_drift=VALID_DRIFT + "\n") + out = pr_comment.render_comment(base, head) + self.assertIn("1 new drift", out) + + +class RenderTests(unittest.TestCase): + def test_render_produces_markdown_with_decisions_and_drifts(self): + import render # noqa: WPS433 + with TemporaryDirectory() as d: + mem = write_memory( + Path(d), + decisions=VALID_DECISION + "\n", + drift=VALID_DRIFT + "\n", + ) + out = render.render(mem) + self.assertIn("# Memory journal", out) + self.assertIn("1 decision(s)", out) + self.assertIn("1 drift(s)", out) + self.assertIn("**decision**", out) + self.assertIn("**drift**", out) + self.assertIn("2026-05-19", out) + + def test_render_handles_empty_memory(self): + import render + with TemporaryDirectory() as d: + mem = write_memory(Path(d)) + out = render.render(mem) + self.assertIn("0 decision(s)", out) + self.assertIn("0 drift(s)", out) + + +if __name__ == "__main__": + unittest.main()