diff --git a/.claude/rules/platform/async-long-tasks.md b/.claude/rules/platform/async-long-tasks.md new file mode 100644 index 0000000..a876c9a --- /dev/null +++ b/.claude/rules/platform/async-long-tasks.md @@ -0,0 +1,10 @@ +# Async Long Tasks + +Any operation taking >30 seconds MUST run asynchronously: +- HTTP batch operations (enrichment, liveness, collect) +- Ralphex launches +- Large test suites on real data + +**How:** Use `Bash` with `nohup ... &`, then immediately respond to the user. Check progress when asked. + +**Never** block the conversation waiting for a long operation to finish. The user must always be able to interact. diff --git a/.claude/rules/platform/communication.md b/.claude/rules/platform/communication.md new file mode 100644 index 0000000..5194efb --- /dev/null +++ b/.claude/rules/platform/communication.md @@ -0,0 +1,23 @@ +# Communication + +## External vs Internal + +- **Safe to do freely:** read files, explore, organize, search the web, work within workspace +- **Ask first:** sending emails, tweets, public posts — anything that leaves the machine + +## Group Chats + +You're a participant, not Ninja's voice or proxy. Don't share his private info. +- Respond when: directly asked, can add genuine value +- Stay silent when: casual banter, someone already answered, you'd just be noise +- Quality > quantity. Use reactions to acknowledge without interrupting. + +## Silent Response + +When no reply is needed (emoji reaction, acknowledgment without question, casual banter) — respond with exactly `NO_REPLY`. The bot swallows this token and sends nothing to the user. Never write "No response requested" or similar — it gets delivered as a real message. + +## Telegram Formatting + +- Bot sends messages as HTML (not MarkdownV2). Use standard Markdown in your output — the bot converts it to HTML automatically. +- Keep messages under 4096 chars (deliver.sh handles splitting). +- Do NOT escape dots, parentheses, or other MarkdownV2 special characters — they will appear as literal backslashes. diff --git a/.claude/rules/platform/delegation.md b/.claude/rules/platform/delegation.md new file mode 100644 index 0000000..b81b6ea --- /dev/null +++ b/.claude/rules/platform/delegation.md @@ -0,0 +1,27 @@ +# Delegation + +## Planning vs Implementation — Strict Separation + +Planning and implementation are **separate processes**. Never auto-transition from planning to coding. + +- **Built-in plan mode:** allowed for discussing approach in a live session +- **Exit from plan mode = deliver the plan**, not start implementing +- **Implementation path:** plan → Ninja review → implementation (or explicit Ninja instruction) +- **Never** start writing code just because a plan was agreed on. The plan needs review first. + +## Sub-Agents + +Claude Code `Agent` tool is the only sub-agent delegation path. + +## Decision Log Check + +Before proposing any new tool, library, or architecture change: +1. Check `reference/governance/decisions.md` for conflicting ACTIVE decisions +2. If conflict exists — do NOT propose. Cite the ADR ID. +3. Include "Read reference/governance/decisions.md" in sub-agent prompts for architectural work. + +## Sub-Agent Management + +- You own the result, not Ninja. If sub-agent fails — fix or report. +- Set explicit time budgets. Unfinished report > timeout. +- Sub-agent results are not visible to user — show a brief summary. diff --git a/.claude/rules/platform/memory-protocol.md b/.claude/rules/platform/memory-protocol.md new file mode 100644 index 0000000..79c1a5c --- /dev/null +++ b/.claude/rules/platform/memory-protocol.md @@ -0,0 +1,35 @@ +# Memory Protocol + +## Storage locations + +- **Daily notes:** `memory/daily/YYYY-MM-DD.md` — consolidation digests +- **Long-term:** `MEMORY.md` — curated index of memory files +- **Auto files:** `memory/auto/` — structured memory files with frontmatter + +## What goes WHERE: rules vs memory + +### Rule (`.claude/rules/custom/`) +**Behavioral instructions.** What to DO or NOT DO. Loaded every session. + +Create a rule when: +- Ninja corrects behavior and it should never happen again +- A process/protocol is established (bugfix flow, verification steps) +- A lesson learned becomes a permanent instruction + +### Memory (`memory/auto/`) +**Context and facts.** What IS, not what to do. Loaded via MEMORY.md index. + +Create a memory when: +- Learning about Ninja (preferences, role, family) → `type: user` +- Project state that's not in code/git (deadlines, decisions, stakeholders) → `type: project` +- External resource locations (Linear boards, Grafana dashboards) → `type: reference` + +### Decision flowchart + +1. Is it an instruction? (do X, don't do Y, always Z) → **RULE** +2. Is it a fact about a person, project, or resource? → **MEMORY** +3. Is it both? → **RULE** (put the instruction in the rule, drop the context if the rule is self-explanatory) +4. Does a rule already cover it? → **Don't create memory. Update the rule if needed.** + +### Never duplicate +Before creating anything — check if a rule or memory already covers it. Same content in two places = guaranteed drift. diff --git a/.ralphex/plans/completed/067-session-defaults-fallback.md b/.ralphex/plans/completed/067-session-defaults-fallback.md deleted file mode 100644 index 3406cbf..0000000 --- a/.ralphex/plans/completed/067-session-defaults-fallback.md +++ /dev/null @@ -1,123 +0,0 @@ -# Plan: sessionDefaults fallback for streamingUpdates and requireMention - -GitHub issue: #67 - -## Problem - -`sessionDefaults.streamingUpdates` and `sessionDefaults.requireMention` are not used as fallbacks by adapters. Both Telegram and Discord adapters hardcode `true` as default when binding doesn't set these values. - -## Goal - -1. Add `streamingUpdates` and `requireMention` to `SessionDefaults` type and validation -2. Use `sessionDefaults` as fallback in adapters when binding doesn't specify a value -3. Change defaults: `streamingUpdates: false`, `requireMention: true` - -## Files to change - -- `bot/src/types.ts` — add fields to `SessionDefaults` interface -- `bot/src/config.ts` — parse new fields in `validateSessionDefaults()`, pass `sessionDefaults` to where adapters are created -- `bot/src/telegram-adapter.ts` — accept `sessionDefaults`, use as fallback for `streamingUpdates` -- `bot/src/discord-adapter.ts` — accept `sessionDefaults`, use as fallback for `streamingUpdates` -- `bot/src/telegram-bot.ts` — pass `sessionDefaults` to adapter; use as fallback for `requireMention` (line 363: `binding.requireMention ?? sessionDefaults.requireMention ?? false`) -- `bot/src/discord-bot.ts` — pass `sessionDefaults` to adapter; use as fallback for `requireMention` (line 81: `binding.requireMention ?? sessionDefaults.requireMention ?? false`) -- Tests — update existing tests, add new ones for fallback behavior - -## Implementation details - -### types.ts - -Add to `SessionDefaults`: -```ts -streamingUpdates: boolean; -requireMention: boolean; -``` - -### config.ts — validateSessionDefaults() - -Add parsing with defaults: -```ts -let streamingUpdates = false; -if (typeof obj.streamingUpdates === "boolean") { - streamingUpdates = obj.streamingUpdates; -} - -let requireMention = true; -if (typeof obj.requireMention === "boolean") { - requireMention = obj.requireMention; -} -``` - -Return them in the result object. - -### telegram-adapter.ts - -Change function signature to accept `sessionDefaults`: -```ts -export function createTelegramAdapter( - ctx: Context, - binding?: TelegramBinding, - threadIdOverride?: number, - sessionDefaults?: SessionDefaults, -): PlatformContext { -``` - -Change line 38: -```ts -streamingUpdates: binding?.streamingUpdates ?? sessionDefaults?.streamingUpdates ?? false, -``` - -### discord-adapter.ts - -Same pattern: -```ts -export function createDiscordAdapter( - channel: DiscordSendableChannel, - binding?: DiscordBinding, - sessionDefaults?: SessionDefaults, -): PlatformContext { -``` - -Change line 29: -```ts -streamingUpdates: binding?.streamingUpdates ?? sessionDefaults?.streamingUpdates ?? false, -``` - -### telegram-bot.ts - -Line 363 — change: -```ts -const requireMention = binding.requireMention ?? config.sessionDefaults.requireMention; -``` - -Pass `config.sessionDefaults` to `createTelegramAdapter()` calls. - -### discord-bot.ts - -Line 81 — change: -```ts -const requireMention = binding.requireMention ?? config.sessionDefaults.requireMention; -``` - -Pass `config.sessionDefaults` to `createDiscordAdapter()` calls. - -### Tests - -- [x] Test that `validateSessionDefaults()` returns correct defaults when not specified -- [x] Test that `validateSessionDefaults()` parses boolean values correctly -- [x] Test telegram adapter uses sessionDefaults.streamingUpdates as fallback -- [x] Test discord adapter uses sessionDefaults.streamingUpdates as fallback -- [x] Test requireMention falls back to sessionDefaults in telegram shouldRespond -- [x] Test requireMention falls back to sessionDefaults in discord shouldRespond -- [x] Test binding-level values override sessionDefaults - -## Checklist - -- [x] Add fields to SessionDefaults type -- [x] Parse in validateSessionDefaults() -- [x] Update telegram-adapter.ts fallback -- [x] Update discord-adapter.ts fallback -- [x] Update telegram-bot.ts requireMention fallback -- [x] Update discord-bot.ts requireMention fallback -- [x] Pass sessionDefaults to adapter creation call sites -- [x] Update/add tests -- [x] All tests pass diff --git a/.ralphex/plans/completed/2026-03-21-unified-config-override-pattern.md b/.ralphex/plans/completed/2026-03-21-unified-config-override-pattern.md deleted file mode 100644 index 1ce207e..0000000 --- a/.ralphex/plans/completed/2026-03-21-unified-config-override-pattern.md +++ /dev/null @@ -1,262 +0,0 @@ -# Unified Config Override Pattern (ADR-064) — Round 1 - -## Goal - -Standardize all configuration override mechanisms in the project to one pattern: upstream ships `X` (tracked, works out of box), user adds `X.local` (gitignored, never conflicts). Currently 6 different patterns coexist, causing confusion and inconsistency. - -## Validation Commands - -```bash -cd ~/src/claude-code-bot && cd bot && npm test && npx tsc --noEmit && cd .. && bash .claude/hooks/guardian.sh 2>/dev/null; echo "exit: $?" -``` - -## Reference: Current Override Patterns - -Six different patterns exist today: - -| # | Pattern | Where Used | How It Works | -|---|---------|-----------|-------------| -| 1 | `.example` copy | config.yaml, crons.yaml, plist, decisions.md | User copies `.example` → removes suffix. Gitignored actual file. | -| 2 | `merge=ours` | CLAUDE.md, USER.md, IDENTITY.md, MEMORY.md, settings.json, .gitignore, .gitattributes | Git merge always keeps workspace version | -| 3 | `X` + `X.local` | settings.json, orphan-allowlist | Deep merge or concatenation | -| 4 | `platform/` + `custom/` | rules | Both auto-loaded, custom = user space | -| 5 | `optional-rules/` → copy to `custom/` | rules | Manual activation | -| 6 | workspace-root > skill-dir fallback | orphan-allowlist (PR#54) | Root files override skill-level entirely | - -## Reference: File Locations and Content - -### config.yaml.example (public repo) -`~/src/claude-code-bot/config.yaml.example:1-4`: -``` -# Minime Bot Configuration -# -# Copy this file to config.yaml and fill in your values: -# cp config.yaml.example config.yaml -``` -Lines 1-100: Full bot config template with placeholder values (`/Users/YOU/`, `YOUR_CHAT_ID`). - -### crons.yaml.example (public repo) -`~/src/claude-code-bot/crons.yaml.example:1-4`: -``` -# Cron job definitions — loaded by cron-runner.ts -# All times use the machine's local timezone -# Copy this file to crons.yaml and customize: -# cp crons.yaml.example crons.yaml -``` -Lines 1-60: Cron template with 3 active examples + 1 commented script-mode example. Uses `YOUR_CHAT_ID` placeholders. - -### .gitattributes (public repo) -`~/src/claude-code-bot/.gitattributes:1-9`: -``` -# Divergent files — keep workspace version on merge -# These files are customized per-workspace and should not be overwritten by upstream -CLAUDE.md merge=ours -USER.md merge=ours -IDENTITY.md merge=ours -MEMORY.md merge=ours -.claude/settings.json merge=ours -.gitignore merge=ours -.gitattributes merge=ours -``` - -### .gitignore (public repo) -`~/src/claude-code-bot/.gitignore:22`: -``` -.claude/settings.local.json -``` -`~/src/claude-code-bot/.gitignore:29`: -``` -orphan-allowlist.local.txt -``` -`~/src/claude-code-bot/.gitignore:39-41`: -``` -# Instance config (user creates from .example) -config.yaml -crons.yaml -``` - -### orphan-allowlist files (current state after PR#54) - -Skill dir (upstream, tracked): -- `~/src/claude-code-bot/.claude/skills/workspace-health/scripts/orphan-allowlist.txt` — 39 lines, platform defaults - -Note: `orphan-allowlist.local.txt` does NOT exist in the public repo skill dir. It exists only in the private workspace's copy at `/.claude/skills/workspace-health/scripts/orphan-allowlist.local.txt` (contains `.playwright-mcp`). This is a workspace-side cleanup item, not a public repo change. - -Workspace root (private workspace): -- `/.orphan-allowlist.local.txt` — hidden file (dot-prefix), inconsistent naming with skill-level `orphan-allowlist.txt` - -### guardian.sh allowlist loading (PR#54) -`~/src/claude-code-bot/.claude/hooks/guardian.sh:79-90`: -```bash -ALLOWLIST_FILES=() -if [[ -f "$WORKSPACE/.orphan-allowlist.txt" ]] || [[ -f "$WORKSPACE/.orphan-allowlist.local.txt" ]]; then - [[ -f "$WORKSPACE/.orphan-allowlist.txt" ]] && ALLOWLIST_FILES+=("$WORKSPACE/.orphan-allowlist.txt") - [[ -f "$WORKSPACE/.orphan-allowlist.local.txt" ]] && ALLOWLIST_FILES+=("$WORKSPACE/.orphan-allowlist.local.txt") -else - SKILL_ALLOWLIST="$WORKSPACE/.claude/skills/workspace-health/scripts/orphan-allowlist.txt" - SKILL_ALLOWLIST_LOCAL="$WORKSPACE/.claude/skills/workspace-health/scripts/orphan-allowlist.local.txt" - [[ -f "$SKILL_ALLOWLIST" ]] && ALLOWLIST_FILES+=("$SKILL_ALLOWLIST") - [[ -f "$SKILL_ALLOWLIST_LOCAL" ]] && ALLOWLIST_FILES+=("$SKILL_ALLOWLIST_LOCAL") -fi -``` - -### orphan-scan.sh allowlist loading (PR#54) -`~/src/claude-code-bot/.claude/skills/workspace-health/scripts/orphan-scan.sh:20-28`: -```bash -if [ -f "$WORKSPACE/.orphan-allowlist.txt" ] || [ -f "$WORKSPACE/.orphan-allowlist.local.txt" ]; then - ALLOWLIST="$WORKSPACE/.orphan-allowlist.txt" - ALLOWLIST_LOCAL="$WORKSPACE/.orphan-allowlist.local.txt" -else - ALLOWLIST="$SCRIPT_DIR/orphan-allowlist.txt" - ALLOWLIST_LOCAL="$SCRIPT_DIR/orphan-allowlist.local.txt" -fi -``` - -### settings.json (public repo) -`~/src/claude-code-bot/.claude/settings.json` — 61 lines. Contains hooks config, outputStyle, autoMemoryEnabled. Protected by `merge=ours`. - -### settings.local.json.example (public repo) -`~/src/claude-code-bot/.claude/settings.local.json.example:1-5`: -```json -{ - "outputStyle": "verbose", - "autoMemoryEnabled": true, - "autoMemoryDirectory": "/absolute/path/to/your/workspace/memory/auto" -} -``` - -### Bot config loading -`~/src/claude-code-bot/bot/src/config.ts` — loads `config.yaml` at runtime. No `.local` merge logic currently exists in bot code. - -`~/src/claude-code-bot/bot/src/cron-runner.ts:17` also reads `config.yaml` directly: -```typescript -const CONFIG_PATH = resolve(REPO_ROOT, "config.yaml"); -``` -Functions `getAgentWorkspace` (lines 117-124) and `loadAdminChatId`/`loadDefaultDelivery` (lines 127-162) parse config.yaml independently — they do not use `loadConfig()` from config.ts. - -### Cron loading -`~/src/claude-code-bot/bot/src/cron-runner.ts` — loads `crons.yaml` at runtime. No `.local` merge logic currently exists. - -`~/src/claude-code-bot/bot/scripts/generate-plists.ts:15` also reads `crons.yaml` directly: -```typescript -const CRONS_PATH = resolve(REPO_ROOT, "crons.yaml"); -``` -This script generates launchd plists from cron definitions. If it doesn't read `crons.local.yaml`, user crons won't get plists and won't run. - -## Tasks - -### Task 1: Fix orphan-allowlist naming and location (workspace-voy5, P1) - -**Problem:** After PR#54, three inconsistencies exist: -1. Workspace root file uses dot-prefix (`.orphan-allowlist.local.txt`) while skill-level uses no dot (`orphan-allowlist.txt`). Hidden files are for system internals, not user-editable config. -2. `orphan-allowlist.local.txt` exists in skill dir (`scripts/`) — it should only exist at workspace root. Skill dir = upstream platform defaults only. -3. When workspace root file exists, skill-level is completely ignored — so `.playwright-mcp` entry from skill-level `.local.txt` is silently lost. - -**Evidence:** File listing shows dot-prefix at root: `/.orphan-allowlist.local.txt`. In the private workspace, skill-dir has `orphan-allowlist.local.txt` with `.playwright-mcp` entry that's now unreachable because root file exists. The public repo skill dir does not have a `.local.txt` file. - -**What we want:** -- `orphan-allowlist.txt` at workspace root (no dot, not hidden) = upstream platform defaults. Shipped by upstream, tracked in git. -- `orphan-allowlist.local.txt` at workspace root (no dot, not hidden) = user overrides. Gitignored. -- guardian.sh and orphan-scan.sh use `orphan-allowlist.txt` and `orphan-allowlist.local.txt` from workspace root. -- Both files are read and combined (platform defaults + user additions). - -- [x] `orphan-allowlist.txt` exists at workspace root (not hidden, tracked in git) -- [x] `orphan-allowlist.local.txt` is gitignored (not hidden, in workspace root) -- [x] `orphan-allowlist.txt` no longer exists in `.claude/skills/workspace-health/scripts/` -- [x] guardian.sh reads allowlists from workspace root only (no fallback to skill dir) -- [x] orphan-scan.sh reads allowlists from workspace root only (no fallback to skill dir) -- [x] Both files are concatenated (user additions extend platform defaults, not replace) -- [x] Add tests for allowlist loading and concatenation (both files combined, local extends platform) -- [x] Verify existing tests pass - -### Task 2: Implement config.yaml + config.local.yaml layering (workspace-voy5, P1) - -**Problem:** Currently `config.yaml.example` must be manually copied and fully filled in by the user. The example contains working defaults (tokenService, sessionDefaults, metricsPort) mixed with placeholder values (agent paths, chat IDs). User must edit the entire file even though most of it is boilerplate. - -**Evidence:** `config.yaml.example:3-4` says "Copy this file to config.yaml and fill in your values". `.gitignore:40` ignores `config.yaml`. New users must copy and edit 100 lines when they only need to specify ~5 values (agent paths, chat IDs). - -**What we want:** -- `config.yaml` = upstream defaults (tracked in git, works out of box for basic setup minus user-specific values) -- `config.local.yaml` = user overrides (gitignored, only user-specific values: agent workspaceCwd, bindings with real chat IDs, discord config) -- User overrides in `config.local.yaml` take precedence over defaults in `config.yaml` -- `config.yaml.example` removed — `config.yaml` itself IS the documented default -- `config.local.yaml.example` added — shows what users typically override -- `.gitignore` updated: remove `config.yaml`, add `config.local.yaml` - -- [x] `config.yaml` is tracked in git with working defaults (no placeholder values) -- [x] `config.local.yaml` is gitignored -- [x] `config.local.yaml.example` exists with clear examples of user overrides -- [x] `config.yaml.example` is removed -- [x] Nested config values in `config.local.yaml` override corresponding values in `config.yaml` without losing unrelated keys -- [x] Bot starts successfully with only `config.yaml` (no local override) — using defaults -- [x] Bot starts successfully with `config.yaml` + `config.local.yaml` — local values override defaults -- [x] Merge precedence: `config.local.yaml` values always win over `config.yaml` -- [x] All config consumers (config.ts, cron-runner.ts) use merged config — no direct reads of `config.yaml` alone -- [x] `.gitignore` lists `config.local.yaml` instead of `config.yaml` -- [x] Add tests for config merging logic -- [x] Verify existing tests pass - -### Task 3: Implement crons.yaml + crons.local.yaml layering (workspace-voy5, P1) - -**Problem:** Same issue as config.yaml — `crons.yaml.example` must be fully copied. The example has useful documentation and structure but placeholder values. User crons are completely separate from upstream example crons. - -**Evidence:** `crons.yaml.example:3-4` says "Copy this file to crons.yaml and customize". `.gitignore:41` ignores `crons.yaml`. Workspace has 650+ line `crons.yaml` with 35+ crons — all user-specific. - -**What we want:** -- `crons.yaml` = upstream examples/documentation (tracked, a few example crons that work with defaults) -- `crons.local.yaml` = user crons (gitignored, all user-specific cron definitions) -- Crons defined in both files are all available at runtime -- If same cron `name` appears in both files, local wins (override mechanism) -- `crons.yaml.example` removed — `crons.yaml` itself IS the documented default -- `crons.local.yaml.example` added — shows format for user crons -- `.gitignore` updated: remove `crons.yaml`, add `crons.local.yaml` - -- [x] `crons.yaml` is tracked in git with documented example crons -- [x] `crons.local.yaml` is gitignored -- [x] `crons.local.yaml.example` exists with clear user cron examples -- [x] `crons.yaml.example` is removed -- [x] Cron loader concatenates cron arrays from both files -- [x] Duplicate cron names: local wins over upstream -- [x] Bot starts with only `crons.yaml` (no local) — example crons loaded (or disabled by default) -- [x] Bot starts with both files — all crons from both are available -- [x] generate-plists.ts reads and merges both crons.yaml and crons.local.yaml when generating plists -- [x] `.gitignore` lists `crons.local.yaml` instead of `crons.yaml` -- [x] Add tests for cron merging logic -- [x] Verify existing tests pass - -### Task 4: Clean up .gitattributes and .example files (workspace-voy5, P2) - -**Problem:** After tasks 1-3, some `.gitattributes` entries and `.example` files are no longer needed. Multiple test files and scripts reference `.example` files that will be removed — these will break. `merge=ours` for `.claude/settings.json` is still needed (ADR-045 — contains hooks that are workspace-customized). Identity files (CLAUDE.md, USER.md, IDENTITY.md, MEMORY.md) still need `merge=ours` — they are workspace-owned, not layered. - -**Evidence:** `.gitattributes:7` lists `.claude/settings.json merge=ours`. ADR-045 explains why: settings.json contains hooks which are workspace-specific. `.gitattributes:8-9` list `.gitignore` and `.gitattributes` themselves — needed to protect the protection mechanism. - -Files referencing `.example` that will break: -- `bot/src/__tests__/project-naming.test.ts:16,112-119,123-130` — reads `config.yaml.example`, asserts it exists at repo root -- `bot/src/__tests__/cron-fields.test.ts:41,49` — reads `crons.yaml.example` -- `.claude/skills/workspace-health/tests/test-scripts.sh:508` — checks `crons.yaml.example` -- `.claude/skills/memory-consolidation/tests/test-platform-integration.sh:43-46` — checks `crons.yaml.example` exists -- `.claude/skills/workspace-health/scripts/config-check.sh:73` — references `settings.local.json.example` -- `CLAUDE.md:56` — references `.claude/settings.local.json.example` - -**What we want:** -- Remove `config.yaml.example` (replaced by tracked `config.yaml`) -- Remove `crons.yaml.example` (replaced by tracked `crons.yaml`) -- Remove `.claude/settings.local.json.example` (pattern is now documented, example adds noise) -- Keep `bot/telegram-bot.plist.example` (plist has machine-specific paths, not layerable) -- Keep `reference/governance/decisions.md.example` (ADR template, not a config) -- Keep all identity-file `merge=ours` entries (CLAUDE.md, USER.md, IDENTITY.md, MEMORY.md) -- Keep `.claude/settings.json merge=ours` (ADR-045) -- Keep `.gitignore merge=ours` and `.gitattributes merge=ours` (self-protection) -- Update CLAUDE.md setup instructions to reference new pattern -- Update README if it references `.example` copy workflow - -- [x] `config.yaml.example` no longer exists in repo -- [x] `crons.yaml.example` no longer exists in repo -- [x] `.claude/settings.local.json.example` no longer exists in repo -- [x] `bot/telegram-bot.plist.example` still exists (not part of this migration) -- [x] `reference/governance/decisions.md.example` still exists (not part of this migration) -- [x] `.gitattributes` retains identity files + settings.json + .gitignore + .gitattributes -- [x] Setup documentation updated to describe `X` + `X.local` pattern -- [x] All test files updated to reference `config.yaml`/`crons.yaml` instead of `.example` variants -- [x] No broken references to removed `.example` files anywhere in codebase (verified by grep) -- [x] Verify existing tests pass diff --git a/.ralphex/plans/completed/memory-consolidation-platform.md b/.ralphex/plans/completed/memory-consolidation-platform.md deleted file mode 100644 index 3540e38..0000000 --- a/.ralphex/plans/completed/memory-consolidation-platform.md +++ /dev/null @@ -1,263 +0,0 @@ -# Add memory-consolidation skill to platform — Round 1 - -## Goal - -Add a memory-consolidation skill that runs as a nightly cron and crystallizes session transcripts into organized persistent memory. Without automated consolidation, workspaces silently lose institutional knowledge: facts from conversations vanish between sessions, MEMORY.md goes stale, and the agent forgets what happened yesterday. GitHub issue #19. - -## Validation Commands - -```bash -cd ~/src/claude-code-bot - -# SKILL.md exists and is well-formed -test -f .claude/skills/memory-consolidation/SKILL.md && echo "PASS" || echo "FAIL: no SKILL.md" - -# Helper scripts exist and are executable -find .claude/skills/memory-consolidation/scripts/ -name "*.sh" -executable | head -5 -test -d .claude/skills/memory-consolidation/scripts/ && echo "PASS: scripts dir exists" || echo "FAIL: no scripts dir" - -# Scripts accept workspace path and run without error -for script in .claude/skills/memory-consolidation/scripts/*.sh; do - echo "--- $(basename "$script") ---" - bash "$script" "$(pwd)" 2>&1 | head -3 -done - -# No hardcoded user-specific paths -grep -r '/Users/' .claude/skills/memory-consolidation/ && echo "FAIL" || echo "PASS: no hardcoded paths" - -# SKILL.md uses portable script paths -grep 'CLAUDE_SKILL_DIR' .claude/skills/memory-consolidation/SKILL.md | head -3 - -# memory-protocol updated -grep 'diary' .claude/optional-rules/memory-protocol.md | head -3 - -# setup.sh handles skill scripts and diary directory -grep -q 'skills' setup.sh && echo "PASS" || echo "FAIL: setup.sh missing skills" -grep -q 'diary' setup.sh && echo "PASS" || echo "FAIL: setup.sh missing diary" - -# crons.yaml.example has consolidation cron -grep -A3 'memory-consolidation' bot/crons.yaml.example - -# .gitignore covers consolidation state -grep 'consolidation' .gitignore - -# Run tests -cd bot && npm test -``` - -## Reference: Claude Code session JSONL format - -Session transcripts are stored at `~/.claude/projects//*.jsonl`. The workspace path is converted by replacing `/` with `-` and prefixing with `-` (e.g., `/Users/user/.minime/workspace` → `-Users-user--minime-workspace`). - -Each JSONL file = one session. First line is always `type: "queue-operation"` with the initial prompt in `content`: - -```json -{"type":"queue-operation","operation":"enqueue","timestamp":"2026-03-14T14:09:41.509Z","sessionId":"1e3c5432...","content":"[Chat: Minime HQ | From: User (@)]\nhey"} -``` - -Human messages have `type: "user"` with `message.role: "user"`: -```json -{"type":"user","message":{"role":"user","content":"[Chat: Minime HQ | From: User (@)]\nhey"},"timestamp":"2026-03-14T14:09:41.528Z","userType":"external","sessionId":"1e3c5432..."} -``` - -Assistant messages have `type: "assistant"` at top level and contain `message.role: "assistant"`: -```json -{"type":"assistant","message":{"model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"🏄‍♂️"}]},"timestamp":"..."} -``` - -**Human session identification:** First line's `content` field starts with `[Chat:` for bot-originated human conversations. Cron/automated sessions start with other patterns (e.g., `IMPORTANT:`, `External code review`, `Second code review pass`, `Code review of:`). - -**Key fields for extraction:** `type`, `message.role`, `message.content`, `timestamp`, `sessionId`. - -## Reference: Public repo current structure - -``` -.claude/ -├── hooks/ (4 scripts: auto-stage, inject-message, session-end-commit, session-start-recovery) -├── rules/ -│ ├── platform/ (2 rules: safety, no-nested-cli) -│ ├── custom/ (.gitkeep — user rules go here, gitignored) -├── optional-rules/ (4 opt-in rules: async-long-tasks, communication, memory-protocol, task-tracking) -├── settings.json (hooks config, uses $CLAUDE_PROJECT_DIR) -└── settings.local.json.example -setup.sh (chmod hooks, create dirs, offer optional rules) -bot/crons.yaml.example (has backup-git and weekly-health stubs) -memory/ (.gitkeep only — gitignored contents) -MEMORY.md (empty template: "Curated index of memory files in memory/") -.gitignore (gitignores: memory/*, reference/, .claude/rules/custom/*, etc.) -``` - -No `.claude/skills/` directory exists on main yet. The workspace-health skill (PR pending) will be the first; memory-consolidation will be the second. - -## Reference: Claude Code skill discovery - -From Claude Code documentation: skills are discovered at `.claude/skills//SKILL.md` — one level deep only. The `${CLAUDE_SKILL_DIR}` variable resolves to the directory containing SKILL.md at runtime, enabling portable references to co-located scripts. This is the same mechanism used by the workspace-health skill on the `workspace-health-platform` branch (verified: `grep 'CLAUDE_SKILL_DIR' .claude/skills/workspace-health/SKILL.md` returns multiple hits on that branch). - -## Reference: memory-protocol optional rule (current) - -`.claude/optional-rules/memory-protocol.md` (20 lines): -```markdown - -# Memory Protocol - -## Why Memory Matters -Claude Code sessions are stateless — each conversation starts fresh... - -## Structure -- **Long-term index:** `MEMORY.md` — curated index of memory files in `memory/` -- **Memory files:** `memory/*.md` — individual notes on topics worth remembering across sessions -- **Daily notes (optional):** `memory/daily/YYYY-MM-DD.md` — consolidation digests - -## Guidelines -- Write memories for anything that should survive across sessions... -- Keep `MEMORY.md` concise — it's an index, not a journal. -- Review and prune stale memories periodically. -- Don't duplicate what's already in code, git history, or documentation. -``` - -Currently references `memory/daily/` — the platform convention is `memory/diary/`. - -## Reference: Memory file format (memory/auto/) - -Individual memory files use YAML frontmatter with `name`, `description`, `type` fields: - -```markdown ---- -name: coffee-shop-investment -description: Инвестиция в кофейню — , , , СПб -type: project ---- - -User инвестировал в кофейню (). Договор подписан . -... -``` - -Types: `user`, `project`, `reference`, `feedback`. The `description` field is used to decide relevance in future conversations. Body contains the full content, often with `**Why:**` and `**How to apply:**` sections for feedback/project types. - -MEMORY.md is an index pointing to these files: -```markdown -# Memory Index -Curated index of memory files in `memory/`. - -``` - -## Reference: Private implementation structure (source material) - -The private workspace has a 731-line SKILL.md at `~/.minime/workspace/.claude/skills/memory-consolidation/SKILL.md` with 5 profile YAML files. The skill needs to be decomposed — core pipeline becomes the platform skill, task/reminder integrations become separate private skills. - -### Pipeline phases in the private implementation - -- **Phase 0: Validate** — check workspace, memory file, lock, cross-skill mutex -- **Phase A: Gather** — read session JSONL files, read current MEMORY.md, read task systems, read git log -- **Phase B: Diff** — compare extracted facts against current memory state, assign confidence scores -- **Phase C: Fix** — apply safe-edits to MEMORY.md and memory files (backup/verify/rollback), create diary entry -- **Phase D: Report & Cleanup** — write diary digest, release locks, update state - -### What the platform skill keeps (generic) - -- Session JSONL discovery and parsing (auto-derive path from workspace) -- Human session filtering (`[Chat:` prefix detection) -- Fact extraction with confidence scoring -- MEMORY.md safe-edit with backup/verify/rollback -- memory/auto/ file creation/updates with frontmatter -- memory/diary/ creation (narrative digest) -- Atomic locking (mkdir-based with stale TTL) -- Cross-skill mutex (.maintenance.lock) -- Mutation limits (default 5 per run) -- Coverage-based permission gradation (full/truncated/error) - -### What the platform skill drops (workspace-specific) - -- Profile system (5 YAML files with per-agent config) -- Task systems (beads, task_index, reminders) — out of scope -- Memory markers (managed sections for specific agents) -- Preflight scripts (custom Python per-agent) -- Watermark/incremental model — replaced by 48h window -- Fast-path heuristics (task stale check, git clean check) -- Repo sync (git pull) — workspace-health handles this -- Legacy session fallback (sessions_history API) - -## Reference: .gitignore current state - -``` -# Workspace user data -memory/* -!memory/.gitkeep -.claude/settings.local.json -.claude/rules/custom/* -!.claude/rules/custom/.gitkeep -reference/ -``` - -The `memory/*` gitignore with `!memory/.gitkeep` exception means all memory content (auto/, diary/, state files) is already gitignored. No new gitignore entries needed for memory subdirectories — only for consolidation state files that live outside memory/. - -## Reference: setup.sh current state - -`setup.sh` (89 lines) handles: jq check, chmod hooks, npm install, create memory/ dir, create rules/custom/ dir, offer optional rules activation, remind to edit USER.md/IDENTITY.md. The workspace-health branch adds chmod for skill scripts. Memory-consolidation needs setup.sh to also create `memory/auto/` and `memory/diary/` subdirectories. - -## Tasks - -### Task 1: Create SKILL.md and helper scripts (#19, P1) - -The public repo has no memory-consolidation skill. The SKILL.md orchestration document and supporting scripts need to be created from scratch, based on the private 731-line implementation but stripped of all workspace-specific features (profiles, tasks, reminders, markers, watermarks). - -The private implementation uses profiles to configure per-agent behavior. The platform version uses zero-config auto-discovery instead — session path derived from workspace path, all defaults work out of the box. - -The private implementation uses watermark-based incremental processing. The platform version uses a simple 48h window (today + yesterday sessions) every run — no state tracking needed for session processing. - -The private implementation's Phase A reads task systems (beads, reminders, task_index). The platform version reads only sessions and memory — task management is out of scope. - -Philosophy: consolidation is sleep for the agent — absorption and crystallization of information, not mechanical fact transfer. The skill should understand what new information means in context of existing memory, update stale entries, resolve contradictions, and produce diary entries as narrative digests. - -What we want: -- A SKILL.md and supporting scripts that work on any fresh workspace clone without configuration -- Recent human sessions (today + yesterday, 48h window) are discovered and processed automatically — no watermarks or state tracking for session processing -- Only human conversations are processed; cron/automated sessions are excluded -- Only high-confidence facts (>= 0.9) trigger automatic memory edits; lower-confidence items noted in diary for manual curation -- Memory edits are safe: failures don't leave MEMORY.md or memory files in a broken state -- Runaway mutations are bounded (default 5 per run), and any mutation failure stops further edits -- Partial failures degrade gracefully — if session reading fails, the skill still writes a diary entry noting the failure -- Concurrent runs are prevented, with stale-lock recovery so a crashed run doesn't permanently block future runs -- workspace-health's `.maintenance.lock` is respected (cross-skill mutex) -- Diary entries are narrative digests of what was learned and what changed — not raw fact dumps -- CLAUDE.md, USER.md, and IDENTITY.md are never modified -- Silent operation — never sends messages to any chat, always NO_REPLY for cron context - -- [x] SKILL.md exists at `.claude/skills/memory-consolidation/SKILL.md` with complete pipeline documentation -- [x] Recent human sessions (48h window) are discovered from any workspace path without configuration -- [x] Cron/automated sessions are correctly filtered out (only `[Chat:` prefixed sessions processed) -- [x] Concurrent runs are prevented with stale-lock recovery -- [x] workspace-health's `.maintenance.lock` is respected before acquiring consolidation lock -- [x] No hardcoded user-specific paths anywhere in skill files -- [x] Script references are portable and resolve correctly from any workspace clone -- [x] MEMORY.md edits are safe: backup before changes, verify after (file non-empty, size reasonable), rollback on failure -- [x] memory/auto/ files use correct frontmatter format (name, description, type) -- [x] memory/diary/ entries are narrative digests, not raw fact lists -- [x] Mutation limit enforced (default 5, stop-on-failure) -- [x] Partial failures degrade gracefully (diary-only on read errors, append-only on partial data) -- [x] Skill does not reference task systems (beads, reminders, task_index) -- [x] Skill does not reference profiles or memory markers -- [x] All scripts run without error on the public repo workspace (`bash script.sh "$(pwd)"`) -- [x] Add tests for helper scripts -- [x] Verify existing tests pass - -### Task 2: Update platform integration (#19, P2) - -The setup.sh, crons.yaml.example, .gitignore, and memory-protocol optional rule need updates to support the new skill. Without these, a fresh workspace clone won't have the right directory structure, won't know how to schedule the cron, and the memory-protocol rule still references the old `memory/daily/` path. - -What we want: -- `setup.sh` creates `memory/auto/` and `memory/diary/` subdirectories during workspace setup -- `setup.sh` makes skill scripts executable (if workspace-health branch isn't merged yet, add this; if already present, verify it covers the new skill) -- `crons.yaml.example` has a `memory-consolidation` cron entry with appropriate schedule (nightly, e.g., 2:00 AM) and adequate timeout for AI processing -- `.gitignore` covers `.consolidation.lock` and `.consolidation-state.json` if they appear at workspace root (note: if they're under `memory/`, they're already covered by `memory/*`) -- `memory-protocol.md` optional rule references `memory/diary/` instead of `memory/daily/` -- `setup.sh` runs without error on a fresh clone - -- [x] `setup.sh` creates `memory/auto/` and `memory/diary/` directories -- [x] Skill scripts in `.claude/skills/*/scripts/` are made executable by `setup.sh` -- [x] `crons.yaml.example` has `memory-consolidation` entry with nightly schedule and adequate timeout -- [x] `.gitignore` covers consolidation lock and state files -- [x] `memory-protocol.md` optional rule references `memory/diary/` (not `memory/daily/`) -- [x] `bash setup.sh` runs without error on a fresh clone -- [x] Add tests verifying setup.sh creates memory subdirectories and crons.yaml.example parses correctly -- [x] Verify existing tests pass diff --git a/.ralphex/plans/completed/named-sessions-status.md b/.ralphex/plans/completed/named-sessions-status.md deleted file mode 100644 index 5a9fffa..0000000 --- a/.ralphex/plans/completed/named-sessions-status.md +++ /dev/null @@ -1,49 +0,0 @@ -# Add /rename command for named sessions - -## Goal - -Add `/rename ` command so users can name sessions and resume them from console via `claude --resume `. - -## Validation Commands - -```bash -cd ./bot -npx tsc --noEmit -npm test -``` - -## Reference: Claude CLI --name flag - -``` --n, --name Set a display name for this session (shown in /resume and terminal title) ---session-id Use a specific session ID (must be a valid UUID) --r, --resume [value] Resume by session ID, or open interactive picker with optional search term -``` - -`--name` sets a display name independently of the UUID. `claude --resume ` matches by display name in interactive mode. `--name` can be combined with `--resume `. - -## Reference: Current code - -`cli-protocol.ts` lines 11-20: `SpawnOptions` — no `name` field. `buildSpawnArgs()` (lines 25-76) does not pass `--name`. - -`types.ts` lines 75-80: `SessionState { sessionId, chatId, agentId, lastActivity }` — no name stored. - -`telegram-bot.ts` lines 27-31: `BOT_COMMANDS` registers start, reset, status — no rename. - -## Tasks - -### Task 1: Add /rename command (#34, P1) - -No way to name sessions. Users must use UUIDs to resume from console. `/rename ` should associate a display name with the session and pass it to the CLI via `--name`. - -What we want: `/rename ` command that names the current session. The name is passed to the Claude CLI as `--name` so `claude --resume ` works from interactive console. Session ID stays UUID, internal routing unchanged. - -- [ ] `/rename ` command exists and responds with confirmation -- [ ] `claude --resume ` works from interactive console after rename -- [ ] Name persists across bot restarts -- [ ] `/rename` without argument shows current session name -- [ ] Invalid names rejected (empty, whitespace-only) -- [ ] `/rename` registered in Telegram command menu -- [ ] `/status` shows session name when set -- [ ] Add tests -- [ ] Verify existing tests pass diff --git a/.ralphex/plans/completed/platform-safety-hooks.md b/.ralphex/plans/completed/platform-safety-hooks.md deleted file mode 100644 index b701cbe..0000000 --- a/.ralphex/plans/completed/platform-safety-hooks.md +++ /dev/null @@ -1,145 +0,0 @@ -# Platform Safety Hooks - -**Issue:** https://github.com/fitz123/claude-code-bot/issues/22 -**Branch:** `platform-safety-hooks` -**Base:** `main` - -## Context - -The workspace template has 4 hooks but lacks two safety hooks that prevent workspace degradation: -1. **protect-files.sh** — blocks cron/autonomous sessions from modifying skill files -2. **guardian.sh** — blocks creation of new files in workspace root outside the allowed structure - -Both have been running in production since mid-March 2026. This task upstreams them to the platform template. - -## Tasks - -### Task 1: Add protect-files.sh and guardian.sh hooks - -**Priority:** P1 -**Files to create:** -- `.claude/hooks/protect-files.sh` -- `.claude/hooks/guardian.sh` - -**Requirements:** -- protect-files.sh: Block writes to `.claude/skills/*` when `$CRON_NAME` env var is set. Exit 0 if no file_path or not a skill path. Exit 2 to block. -- guardian.sh: Fail-closed design. Block Write tool creating new files in workspace root if the root-level path component is not in the orphan-allowlist.txt. Edit tool always allowed. Overwriting existing files always allowed. Uses `$CLAUDE_PROJECT_DIR` for workspace root (not hardcoded path). Depends on `jq`. Loads allowlist from `.claude/skills/workspace-health/scripts/orphan-allowlist.txt`. Supports exact and glob matching. Blocks path traversal (`..`). -- Both scripts must be POSIX-compatible bash, use `jq` for JSON parsing of hook input -- Hook input format: JSON on stdin with `tool_name` and `tool_input.file_path` fields - -**Acceptance criteria:** -- [ ] protect-files.sh blocks skill file writes when CRON_NAME is set -- [ ] protect-files.sh allows skill file writes when CRON_NAME is unset -- [ ] guardian.sh blocks new files outside allowlist -- [ ] guardian.sh allows overwrites of existing files -- [ ] guardian.sh allows Edit tool unconditionally -- [ ] guardian.sh fails closed when jq is missing -- [ ] guardian.sh fails closed when allowlist is missing -- [ ] guardian.sh blocks path traversal attempts -- [ ] No hardcoded workspace paths (uses $CLAUDE_PROJECT_DIR) -- [ ] Both scripts are executable - -### Task 2: Update settings.json to register new hooks - -**Priority:** P1 -**Files to modify:** -- `.claude/settings.json` - -**Requirements:** -- Add a second PreToolUse entry with matcher `Edit|Write` containing both `protect-files.sh` and `guardian.sh` -- Keep existing PreToolUse entry for `inject-message.sh` (matcher `*`) unchanged -- Hook command format: `"$CLAUDE_PROJECT_DIR"/.claude/hooks/