diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9528c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: + - master + - "codex/**" + - "harden-*" + pull_request: + workflow_dispatch: + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install test and build tooling + run: | + python -m pip install --upgrade pip + python -m pip install pytest build twine + + - name: Run test suite + run: python -m pytest -q + + - name: Build distributions + run: python -m build + + - name: Check distribution metadata + run: python -m twine check dist/* + + - name: Smoke test installed package + run: | + python -m venv .venv-ci + .venv-ci/bin/python -m pip install dist/*.whl + .venv-ci/bin/sessionanchor --help + .venv-ci/bin/python -m sessionanchor --help diff --git a/src/sessionanchor/init.py b/src/sessionanchor/init.py index 7a7912e..467cba6 100644 --- a/src/sessionanchor/init.py +++ b/src/sessionanchor/init.py @@ -77,25 +77,39 @@ def run_init(repo_root: str | None = None, skip_index: bool = False) -> None: conn.close() + # Verify CLI is discoverable on PATH + cli_on_path = shutil.which("sessionanchor") is not None + quick_start_prefix = "sessionanchor" + print() print("=" * 60) print(" SessionAnchor initialized!") print("=" * 60) + + if not cli_on_path: + quick_start_prefix = " sessionanchor" + print() + print(" [!] NOTE: 'sessionanchor' was not found on PATH.") + print(" Use the same launcher for future commands, e.g.") + print(" 'uvx sessionanchor ...', 'pipx run sessionanchor ...',") + print(" or 'python -m sessionanchor ...'.") + print(" Replace in the examples below with the one you use.") + print() print(" Quick start:") print() print(" # At session start - get your briefing") - print(" sessionanchor boot") + print(f" {quick_start_prefix} boot") print() print(" # Save a decision") - print(' sessionanchor save add --tier L1 --category decision \\') + print(f' {quick_start_prefix} save add --tier L1 --category decision \\') print(' --title "Chose SQLite" --content "Zero deps, local-only"') print() print(" # Search memory") - print(' sessionanchor query "deployment"') + print(f' {quick_start_prefix} query "deployment"') print() print(" # End of session") - print(' sessionanchor save session-end --summary "Built auth flow"') + print(f' {quick_start_prefix} save session-end --summary "Built auth flow"') print() print(" CLAUDE.md has been configured to instruct Claude to use") print(" this memory system automatically.") diff --git a/src/sessionanchor/templates/claude_md_section.txt b/src/sessionanchor/templates/claude_md_section.txt index c61e64c..ae9fe28 100644 --- a/src/sessionanchor/templates/claude_md_section.txt +++ b/src/sessionanchor/templates/claude_md_section.txt @@ -2,53 +2,78 @@ ## Context Management This project uses [SessionAnchor](https://github.com/calesthio/SessionAnchor) for session continuity. -**Do NOT read large context files at session start.** Use the memory system instead. -### Session Start Protocol +### MANDATORY: Session Start (do this FIRST, before any other work) +Run this before doing anything else: ```bash -# 1. Get your briefing (~2000 tokens instead of reading all project files) sessionanchor boot +``` +- If `command not found`: use the same launcher you used for SessionAnchor for all commands below: + `uvx sessionanchor ...`, `pipx run sessionanchor ...`, or `python -m sessionanchor ...`. + If SessionAnchor is not installed yet, run `pip install sessionanchor`, then retry. +- If `No memory database found`: run the matching init command with the same launcher + (`sessionanchor init`, `uvx sessionanchor init`, `pipx run sessionanchor init`, + or `python -m sessionanchor init`), then boot the same way. +- Read the output — it contains prior decisions, active work, and blockers from previous sessions. +- Do NOT skip this. Do NOT read large project files as a substitute. -# 2. If you need deeper context on any topic during the session: -sessionanchor query "search terms" +### MANDATORY: Save After Every Substantive Action -# 3. If you need to understand code structure: -sessionanchor index find "topic" -sessionanchor index map -``` +You MUST save context after each of these — do NOT batch, do NOT wait for session end: +- A feature, fix, or deliverable is completed +- A decision is made (technical choice, architecture, tradeoff) +- A blocker is discovered +- You learn something non-obvious about the codebase + +**How to save:** +- If your platform supports background agents: spawn one with the save command +- Otherwise: run the save command inline before continuing -### Context Saving (MANDATORY - do not skip) +```bash +# Decision made +sessionanchor save add --tier L1 --category decision --title "..." --content "..." -**Save context continuously, not just at session end.** After completing any substantive work -(a feature, a fix, a decision, a research task), immediately write the relevant context to memory. -Do NOT wait for the user to ask. Do NOT wait for "end of session." The user may close the -window at any time. +# Task/feature completed (marks the action item as completed) +sessionanchor save complete --title "..." --category action_item -**How to save: use a subagent.** Do NOT block the main conversation. Spawn a background agent -with the save commands and continue working. +# Significant event or milestone worth logging +sessionanchor save add --tier L1 --category session_log --title "..." --content "..." -**When to save (trigger immediately after any of these):** -- A decision is made -> `sessionanchor save add --tier L1 --category decision --title "..." --content "..."` -- A task is completed -> `sessionanchor save complete --title "..." --category action_item` -- A new blocker is discovered -> `sessionanchor save add --tier L1 --category blocker --title "..." --content "..."` -- New technical knowledge -> `sessionanchor save add --tier L2 --category technical_note --title "..." --content "..."` -- Significant code changes -> `sessionanchor save add --tier L1 --category session_log --title "..." --content "..."` +# Blocker found +sessionanchor save add --tier L1 --category blocker --title "..." --content "..." -**Session summary (run when work naturally winds down):** -```bash -sessionanchor save session-end --summary "What was accomplished" +# Technical knowledge learned +sessionanchor save add --tier L2 --category technical_note --title "..." --content "..." ``` -### Memory Tiers +**Self-check: if you have completed 3+ tasks without running a single save command, stop and save now.** + +**Scope guard: only save context that is relevant to THIS project.** If the user asks you to +work on an unrelated repo, fix an external tool, or do research that does not affect this +codebase, do NOT save that work here — it is noise. Example: if this project is a web app and +the user asks you to debug a CLI tool in a different repo mid-session, that debugging context +does not belong in this project's memory. -- **L0** (~500 tokens): Identity, current phase, top priorities. Always loaded. -- **L1** (~1500 tokens): Recent decisions, active action items, blockers. Loaded at boot. -- **L2** (on-demand): Full history, completed items, deep technical notes. Query when needed. +### Session End -### Re-indexing +When work winds down: +```bash +sessionanchor save session-end --summary "What was accomplished" +``` + +### Reference: Querying Deep Context -Run after major structural changes: ```bash +# Search memory for a topic +sessionanchor query "search terms" + +# Re-index after major structural changes (new directories, renamed modules) sessionanchor index ``` + +### Reference: Memory Tiers + +- **L0** (~500 tokens): Identity, current phase, top priorities. Always loaded at boot. +- **L1** (~1500 tokens): Recent decisions, active items, blockers. Loaded at boot. +- **L2** (on-demand): Full history, completed items, deep technical notes. Use `sessionanchor query` to retrieve. diff --git a/tests/test_e2e_cli.py b/tests/test_e2e_cli.py index e60de62..945f53d 100644 --- a/tests/test_e2e_cli.py +++ b/tests/test_e2e_cli.py @@ -339,7 +339,7 @@ def test_index_find_and_map_cover_repo_workflow(repo: Path): conn.close() assert "Indexed" in index_result.stdout - assert "src\\auth.py" in find_result.stdout + assert str(Path("src") / "auth.py") in find_result.stdout assert ".env" not in find_result.stdout assert "## Modules" in map_result.stdout assert "## File Tree" in map_result.stdout diff --git a/tests/test_init.py b/tests/test_init.py index 95055a3..912f237 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -48,6 +48,38 @@ def test_init_creates_claude_md(temp_repo): assert "sessionanchor boot" in content +def test_claude_md_has_mandatory_sections(temp_repo): + run_init(repo_root=temp_repo, skip_index=True) + claude_md = os.path.join(temp_repo, "CLAUDE.md") + with open(claude_md) as f: + content = f.read() + # Mandatory start protocol + assert "MANDATORY: Session Start" in content + # Mandatory save protocol + assert "MANDATORY: Save After Every Substantive Action" in content + # Self-check instruction + assert "Self-check" in content + # Install fallback + assert "pip install sessionanchor" in content + # python -m fallback + assert "python -m sessionanchor" in content + # runner fallback for ephemeral installs + assert "uvx sessionanchor" in content + # completion wording should stay accurate + assert "marks the action item as completed" in content + assert "leaves the boot briefing" not in content + + +def test_init_warns_when_cli_not_on_path(temp_repo, capsys, monkeypatch): + import sessionanchor.init as init_module + monkeypatch.setattr(init_module.shutil, "which", lambda _name: None) + run_init(repo_root=temp_repo, skip_index=True) + captured = capsys.readouterr() + assert "not found on PATH" in captured.out + assert "python -m sessionanchor" in captured.out + assert " sessionanchor" in captured.out + + def test_init_patches_existing_claude_md(temp_repo): claude_md = os.path.join(temp_repo, "CLAUDE.md") with open(claude_md, "w") as f: