diff --git a/docs/skills/ce-compound.md b/docs/skills/ce-compound.md index 031c6fefa..66b2b1ddc 100644 --- a/docs/skills/ce-compound.md +++ b/docs/skills/ce-compound.md @@ -62,7 +62,7 @@ Most teams solve the same problem twice — sometimes with the same person — b ### 1. Two modes — Full vs Lightweight, agent-selected -**Full mode** runs three research subagents in parallel (Context Analyzer / Solution Extractor / Related Docs Finder), plus an automatic session-history probe that searches your prior sessions across Claude Code, Codex, and Cursor for related context. Cross-references existing docs, detects duplicates, runs specialized reviews. +**Full mode** runs three research subagents in parallel (Context Analyzer / Solution Extractor / Related Docs Finder), plus an automatic session-history probe that searches your prior sessions across Claude Code, Codex, Cursor, Pi, and oh-my-pi (omp) for related context. Cross-references existing docs, detects duplicates, runs specialized reviews. **Lightweight mode** writes the same solution-doc artifact type in a single pass, with no subagents or cross-referencing. It is lower overhead, but it also skips overlap detection, session-history research, and semantic grounding validation. diff --git a/skills/ce-compound/SKILL.md b/skills/ce-compound/SKILL.md index 1bbb260e6..884650a75 100644 --- a/skills/ce-compound/SKILL.md +++ b/skills/ce-compound/SKILL.md @@ -290,7 +290,7 @@ Pass `{run_id}` and the resolved absolute `{run_dir}` into every Phase 1 subagen else echo "Session history bundled scripts were not found in this skill's directory; skipping the session-history probe for this run."; fi ``` - Pi sessions are included when present under `~/.pi/agent/sessions/`; they carry `cwd` like Codex but no git branch. If `_meta.files_processed` is `0`, return `no relevant prior sessions`. If the first pass finds no relevant branch matches, or if processing Codex or Pi sessions, derive 2-4 keywords from the topic and re-run metadata extraction with `--keyword K1,K2,...`. Keep at most 5 sessions across Claude Code, Codex, Cursor, and Pi, ranked by branch match, keyword match count, file size over 30KB, and recency. Exclude the current session. + Pi sessions are included when present under `~/.pi/agent/sessions/`, and oh-my-pi (`omp`) sessions under `~/.omp/agent/sessions/` (named profiles: `~/.omp/profiles//agent/sessions/`); they carry `cwd` like Codex but no git branch. If `_meta.files_processed` is `0`, return `no relevant prior sessions`. If the first pass finds no relevant branch matches, or if processing Codex, Pi, or oh-my-pi (`omp`) sessions, derive 2-4 keywords from the topic and re-run metadata extraction with `--keyword K1,K2,...`. Keep at most 5 sessions across Claude Code, Codex, Cursor, Pi, and oh-my-pi (`omp`), ranked by branch match, keyword match count, file size over 30KB, and recency. Exclude the current session. **Escalation gate.** The discovery+metadata pass above is the cheap probe and always runs in Full mode. Escalate to the extraction and synthesis stages below **only** when at least one retained candidate clears the relevance bar: a current-branch match, or ≥2 topic-keyword matches. If no candidate clears the bar (including the `_meta.files_processed` is `0` case), stop here, record `no relevant prior sessions` as the session-history input, and skip extraction and synthesis. This gate is what keeps the always-on probe cheap — the expensive synthesis is paid for only when a prior session is genuinely relevant. diff --git a/skills/ce-compound/references/agents/session-historian.md b/skills/ce-compound/references/agents/session-historian.md index 2b8b961c8..a78c5014b 100644 --- a/skills/ce-compound/references/agents/session-historian.md +++ b/skills/ce-compound/references/agents/session-historian.md @@ -1,6 +1,6 @@ **Note: The current year is 2026.** Use this when interpreting session timestamps. -You are an expert at extracting institutional knowledge from coding agent session history. You receive pre-extracted skeleton and error files from the caller's internal session-history flow and synthesize findings about a specific problem or topic — what was learned, tried, decided in prior sessions across Claude Code, Codex, Cursor, and Pi. +You are an expert at extracting institutional knowledge from coding agent session history. You receive pre-extracted skeleton and error files from the caller's internal session-history flow and synthesize findings about a specific problem or topic — what was learned, tried, decided in prior sessions across Claude Code, Codex, Cursor, Pi, and oh-my-pi (omp). Your scope is **synthesis only**. The caller handles discovery, branch/keyword filtering, scan-window selection, deep-dive selection, and per-session extraction before dispatching you. @@ -13,9 +13,9 @@ The dispatch prompt provides: - **`sessions`** — an array of objects (5 max), one per pre-extracted session, each with: - `path` — absolute path to a skeleton text file inside `scratch_dir` - `errors_path` *(optional)* — absolute path to an errors text file when the orchestrator extracted errors-mode for this session - - `platform` — `claude`, `codex`, `cursor`, or `pi` + - `platform` — `claude`, `codex`, `cursor`, `pi`, or `omp` - `branch` — git branch when present (Claude Code only) - - `cwd` — working directory when present (Codex and Pi) + - `cwd` — working directory when present (Codex, Pi, and omp) - `ts` and `last_ts` — session start and last-message timestamps - `match_count` and `keyword_matches` — when keyword filtering was used by the orchestrator - **`output_schema`** *(optional)* — the structure the response should follow. When supplied, honor it verbatim. @@ -28,7 +28,7 @@ If the dispatch prompt arrives without a `sessions` array, or with an empty arra These rules apply at all times during synthesis. -- **Read only the paths the orchestrator gave you.** Use the platform's native file-read tool (e.g., `Read` in Claude Code) on each `path`. Do not read source session files directly under `~/.claude/projects/`, `~/.codex/sessions/`, `~/.cursor/projects/`, or `~/.pi/agent/sessions/` — those are MB-scale and would blow the context window. The orchestrator already extracted what's relevant. +- **Read only the paths the orchestrator gave you.** Use the platform's native file-read tool (e.g., `Read` in Claude Code) on each `path`. Do not read source session files directly under `~/.claude/projects/`, `~/.codex/sessions/`, `~/.cursor/projects/`, `~/.pi/agent/sessions/`, or `~/.omp/agent/sessions/` — those are MB-scale and would blow the context window. The orchestrator already extracted what's relevant. - **Never invoke the Skill tool.** This agent runs in subagent context where Skill calls deadlock. The orchestrator has already done all extraction; you only synthesize. - **Never extract or reproduce tool call inputs/outputs verbatim.** Summarize what was attempted and what happened. - **Never include thinking or reasoning block content.** Claude Code thinking blocks are internal reasoning; Codex reasoning blocks are encrypted. Neither is actionable. The skeleton extractor already strips these — do not surface them if any survived. @@ -50,7 +50,7 @@ Read each `path` in the dispatch payload, then synthesize against the `problem_t - **Decisions and rationale** — Why one approach was chosen over alternatives. - **Error patterns** — Recurring errors across sessions (most visible when the orchestrator supplied an `errors_path` for a session) that indicate a systemic issue. - **Evolution across sessions** — How understanding of the problem changed from session to session, potentially across different tools. -- **Cross-tool blind spots** — When sessions span Claude Code + Codex + Cursor + Pi, look for things the user might not realize from any single tool alone. Complementary work (one tool tackled the schema while the other tackled the API), duplicated effort (same approach tried in both tools days apart), or gaps (neither tool's sessions touched a component that connects the work). Only call out cross-tool observations when genuinely informative — if both sources tell the same story, there's nothing to flag. +- **Cross-tool blind spots** — When sessions span Claude Code + Codex + Cursor + Pi + omp, look for things the user might not realize from any single tool alone. Complementary work (one tool tackled the schema while the other tackled the API), duplicated effort (same approach tried in both tools days apart), or gaps (neither tool's sessions touched a component that connects the work). Only call out cross-tool observations when genuinely informative — if both sources tell the same story, there's nothing to flag. - **Staleness** — Older sessions may reflect conclusions about code that has since changed. When surfacing findings from sessions more than a few days old, consider whether the relevant code or context is likely to have moved on. Caveat older findings rather than presenting them with the same confidence as recent ones. Cite actual evidence from the extracted files, not vibe-summaries. When a finding is anchored in a specific session's content, that session's metadata (platform, branch/cwd, ts) helps the caller locate it. @@ -62,7 +62,7 @@ If the dispatch prompt supplies an `output_schema`, follow it verbatim. Do not a Otherwise, lead with a brief one-line provenance header: ``` -**Sessions read**: [count] ([N] Claude Code, [N] Codex, [N] Cursor, [N] Pi) | [date range] +**Sessions read**: [count] ([N] Claude Code, [N] Codex, [N] Cursor, [N] Pi, [N] omp) | [date range] ``` Then the synthesis prose, organized under the default schema: diff --git a/skills/ce-compound/scripts/session-history/discover-sessions.sh b/skills/ce-compound/scripts/session-history/discover-sessions.sh index 46f426f41..d254405de 100755 --- a/skills/ce-compound/scripts/session-history/discover-sessions.sh +++ b/skills/ce-compound/scripts/session-history/discover-sessions.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Discover session files across Claude Code, Codex, Cursor, and Pi. +# Discover session files across Claude Code, Codex, Cursor, Pi, and oh-my-pi (omp). # -# Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi] +# Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi|omp] # # Outputs one file path per line. Safe in both bash and zsh (all globs guarded). # Pass output to extract-metadata.py: @@ -15,8 +15,8 @@ set -euo pipefail -REPO_NAME="${1:?Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi]}" -DAYS="${2:?Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi]}" +REPO_NAME="${1:?Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi|omp]}" +DAYS="${2:?Usage: discover-sessions.sh [--cwd /abs/repo/root] [--platform claude|codex|cursor|pi|omp]}" PLATFORM="all" REPO_CWD="" @@ -111,17 +111,129 @@ discover_pi() { done } +# --- oh-my-pi (omp) --- +# Encode omp's raw bucket name for a cwd: home-relative "-", +# tmp-relative "-tmp-", and otherwise "----", with path separators +# and ":" encoded as "-" (session-paths.ts getDefaultSessionDirName / +# encodeLegacyAbsoluteSessionDirName). This raw scheme predates the hashed +# scheme and is current again since omp 17.2.9 (#7646 restored it and removed +# automatic migration), so buckets in the wild use both shapes. Canonicalize +# with physical paths so symlinked cwds resolve to the same bucket, mirroring +# omp's resolveEquivalentPath. Prints nothing when the cwd cannot be resolved. +encode_omp_raw_cwd() { + local cwd canon_home canon_tmp rel + cwd="$(cd "$1" 2>/dev/null && pwd -P)" || return 0 + canon_home="$(cd "$HOME" 2>/dev/null && pwd -P)" || canon_home="$HOME" + case "$cwd" in + "$canon_home") + printf -- '-' + ;; + "$canon_home"/*) + rel="$(printf '%s' "${cwd#"$canon_home"/}" | sed 's/[/\\:]/-/g')" + printf -- '-%s' "$rel" + ;; + *) + canon_tmp="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || canon_tmp="" + case "$cwd" in + "$canon_tmp") + printf -- '-tmp' + ;; + "$canon_tmp"/*) + rel="$(printf '%s' "${cwd#"$canon_tmp"/}" | sed 's/[/\\:]/-/g')" + printf -- '-tmp-%s' "$rel" + ;; + *) + rel="$(printf '%s' "${cwd#/}" | sed 's/[/\\:]/-/g')" + printf -- '--%s--' "$rel" + ;; + esac + ;; + esac +} + +discover_omp() { + local config_dir="${PI_CONFIG_DIR:-.omp}" + + # omp's explicit session-dir override stores session files directly in the + # supplied directory (flat), mirroring Pi's override branch. The cwd filter + # later reads each header and keeps only sessions for the active repo. + if [ -n "${PI_CODING_AGENT_SESSION_DIR:-}" ]; then + local base="$PI_CODING_AGENT_SESSION_DIR" + [ -d "$base" ] || return 0 + find "$base" -maxdepth 1 -name "*.jsonl" -mtime "-${DAYS}" 2>/dev/null + if [ -z "$REPO_CWD" ]; then + for dir in "$base"/*"$REPO_NAME"*/; do + [ -d "$dir" ] || continue + find "$dir" -maxdepth 1 -name "*.jsonl" -mtime "-${DAYS}" 2>/dev/null + done + fi + return 0 + fi + + # omp has two bucket-naming schemes in the wild, and both keep the raw + # repo basename (spaces and all) inside the bucket name: + # - raw: "-", "-tmp-", "----" (legacy relative to + # the hashed scheme; restored as current in omp 17.2.9, #7646) + # - hashed: "--" + # (intermediate releases; basename runs of [^a-zA-Z0-9._-] collapse to + # "-", edge dashes stripped, capped at the last 80 chars, empty falls + # back to "project" — session-paths.ts getDefaultSessionDirName) + # Scan basename-matching buckets in the default-profile sessions root and + # in every named-profile root; exact repo attribution comes from the + # downstream header `cwd` filter (extract-metadata.py --cwd-filter reads + # the type:"session" header). Glob the sanitized form so repos whose + # basename contains characters the hashed scheme normalizes (e.g. spaces) + # still match, and glob the raw form so raw-scheme buckets whose basename + # sanitizes differently (e.g. "my repo" in "--Users-test-Code-my repo--") + # are found too. When --cwd is supplied, also probe the exact raw bucket + # name: it catches buckets the basename globs miss when the bucket's path + # segments no longer resemble the repo name as typed. + local sanitized + sanitized="$(printf '%s' "$REPO_NAME" | sed -E 's/[^a-zA-Z0-9._-]+/-/g; s/^-+//; s/-+$//' | tail -c 80)" + [ -n "$sanitized" ] || sanitized="project" + local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/$config_dir/agent}" + { + local root dir encoded + if [ -n "$REPO_CWD" ]; then + encoded="$(encode_omp_raw_cwd "$REPO_CWD")" + if [ -n "$encoded" ]; then + for root in "$agent_dir/sessions" "$HOME/$config_dir"/profiles/*/agent/sessions; do + [ -d "$root/$encoded" ] || continue + find "$root/$encoded" -maxdepth 1 -name "*.jsonl" -mtime "-${DAYS}" 2>/dev/null + done + fi + fi + for root in "$agent_dir/sessions" "$HOME/$config_dir"/profiles/*/agent/sessions; do + [ -d "$root" ] || continue + for dir in "$root"/*"$sanitized"*/; do + [ -d "$dir" ] || continue + find "$dir" -maxdepth 1 -name "*.jsonl" -mtime "-${DAYS}" 2>/dev/null + done + if [ "$REPO_NAME" != "$sanitized" ]; then + for dir in "$root"/*"$REPO_NAME"*/; do + [ -d "$dir" ] || continue + find "$dir" -maxdepth 1 -name "*.jsonl" -mtime "-${DAYS}" 2>/dev/null + done + fi + done + # The probe and the globs can hit the same bucket; emit each path once. + } | awk '!seen[$0]++' +} + # --- Dispatch --- case "$PLATFORM" in claude) discover_claude ;; codex) discover_codex ;; cursor) discover_cursor ;; pi) discover_pi ;; + omp) discover_omp ;; all) - discover_claude - discover_codex - discover_cursor - discover_pi + # Pi and omp share the PI_CODING_AGENT_SESSION_DIR override: when it + # is set, both discover functions emit the same flat-dir files, and + # the downstream xargs call does not deduplicate. Emit each path once; + # platform attribution is unaffected because extract-metadata.py + # detects the file shape (title slot => omp, otherwise pi). + { discover_claude; discover_codex; discover_cursor; discover_pi; discover_omp; } | awk '!seen[$0]++' ;; *) echo "Unknown platform: $PLATFORM" >&2 diff --git a/skills/ce-compound/scripts/session-history/extract-errors.py b/skills/ce-compound/scripts/session-history/extract-errors.py index 7f0a8489f..e2599cb36 100644 --- a/skills/ce-compound/scripts/session-history/extract-errors.py +++ b/skills/ce-compound/scripts/session-history/extract-errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Extract error signals from a Claude Code, Codex, Cursor, or Pi JSONL session file. +"""Extract error signals from a Claude Code, Codex, Cursor, Pi, or oh-my-pi (omp) JSONL session file. Usage: cat | python3 extract-errors.py @@ -163,6 +163,9 @@ def _pi_context_objects(objects): def handle_pi(obj): + # omp's physical type:'title' slot line returns here like any non-message + # entry. type:'title_change' entries are a different pi entry type — they + # return the same way; do not conflate the two when filtering. if obj.get("type") != "message": return msg = obj.get("message", {}) @@ -203,6 +206,9 @@ def handle_pi(obj): # Auto-detect platform from first few lines, then process all detected = None buffer = [] +# omp files physically begin with a fixed-width type:'title' slot line before +# the pi-shaped type:'session' header; bare pi files start with the header. +seen_title_slot = False for line in sys.stdin: line = line.strip() @@ -215,7 +221,9 @@ def handle_pi(obj): try: obj = json.loads(line) if obj.get("type") == "session" and "cwd" in obj: - detected = "pi" + detected = "omp" if seen_title_slot else "pi" + elif obj.get("type") == "title" and len(buffer) == 1: + seen_title_slot = True elif obj.get("type") in ("user", "assistant"): detected = "claude" elif obj.get("type") in ("session_meta", "turn_context", "response_item", "event_msg"): @@ -229,7 +237,7 @@ def handle_pi(obj): def handle_noop(obj): pass -handlers = {"claude": handle_claude, "codex": handle_codex, "cursor": handle_noop, "pi": handle_pi} +handlers = {"claude": handle_claude, "codex": handle_codex, "cursor": handle_noop, "pi": handle_pi, "omp": handle_pi} handler = handlers.get(detected, handle_noop) objects = [] @@ -239,7 +247,7 @@ def handle_noop(obj): except (json.JSONDecodeError, KeyError): stats["parse_errors"] += 1 -if detected == "pi": +if detected in ("pi", "omp"): objects = _pi_context_objects(objects) for obj in objects: diff --git a/skills/ce-compound/scripts/session-history/extract-metadata.py b/skills/ce-compound/scripts/session-history/extract-metadata.py index e24c8f447..192c18ae1 100644 --- a/skills/ce-compound/scripts/session-history/extract-metadata.py +++ b/skills/ce-compound/scripts/session-history/extract-metadata.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Extract session metadata from Claude Code, Codex, Cursor, and Pi JSONL files. +"""Extract session metadata from Claude Code, Codex, Cursor, Pi, and oh-my-pi (omp) JSONL files. Batch mode (preferred — one invocation for all files): python3 extract-metadata.py /path/to/dir/*.jsonl @@ -57,6 +57,36 @@ def try_codex(lines): return meta if meta else None +def try_omp(lines): + """oh-my-pi (omp) sessions: a fixed-width type='title' slot line physically + first, then a pi-shaped type='session' header with cwd. Checked before Pi: + a bare pi file has no title slot and must still detect as pi.""" + seen_first = False + for line in lines: + stripped = line.strip() + if not stripped: + continue + try: + obj = json.loads(stripped) + except (json.JSONDecodeError, KeyError): + if not seen_first: + return None + continue + if not seen_first: + seen_first = True + if obj.get("type") != "title": + return None + continue + if obj.get("type") == "session" and "cwd" in obj: + return { + "platform": "omp", + "cwd": obj.get("cwd", ""), + "session": obj.get("id", ""), + "ts": obj.get("timestamp", ""), + } + return None + + def try_pi(lines): """Pi sessions: type='session' header with cwd, followed by message entries.""" for line in lines: @@ -88,7 +118,7 @@ def try_cursor(lines): def extract_from_lines(lines): - return try_claude(lines) or try_codex(lines) or try_pi(lines) or try_cursor(lines) + return try_claude(lines) or try_codex(lines) or try_omp(lines) or try_pi(lines) or try_cursor(lines) TAIL_BYTES = 16384 # Read last 16KB to find final timestamp past trailing metadata @@ -221,6 +251,7 @@ def _extract_user_assistant_text(filepath): except (json.JSONDecodeError, ValueError): continue + # omp files share the pi-shaped session header, so this covers both. is_pi = any( obj.get("type") == "session" and "cwd" in obj for obj in objects ) diff --git a/skills/ce-compound/scripts/session-history/extract-skeleton.py b/skills/ce-compound/scripts/session-history/extract-skeleton.py index 69353d97d..386a3d0ed 100644 --- a/skills/ce-compound/scripts/session-history/extract-skeleton.py +++ b/skills/ce-compound/scripts/session-history/extract-skeleton.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -"""Extract the conversation skeleton from a Claude Code, Codex, Cursor, or Pi JSONL session file. +"""Extract the conversation skeleton from a Claude Code, Codex, Cursor, Pi, or oh-my-pi (omp) JSONL session file. Usage: cat | python3 extract-skeleton.py cat | python3 extract-skeleton.py --output PATH -Auto-detects platform (Claude Code, Codex, Cursor, or Pi) from the JSONL structure. +Auto-detects platform (Claude Code, Codex, Cursor, Pi, or oh-my-pi (omp)) from the JSONL structure. Extracts: - User messages (text only, no tool results) - Assistant text (no thinking/reasoning blocks) @@ -342,7 +342,12 @@ def _pi_context_objects(objects): def handle_pi(obj): - """Pi sessions: type='message' with message.role and content blocks.""" + """Pi/omp sessions: type='message' with message.role and content blocks. + + omp's physical type:'title' slot line lands here too and returns below + like any non-message entry. type:'title_change' entries are a different + pi entry type — they also carry no message payload and return the same + way; do not conflate the two when filtering.""" entry_type = obj.get("type") ts = obj.get("timestamp", "")[:19] @@ -520,6 +525,9 @@ def handle_cursor(obj): # Auto-detect platform from first few lines, then process all detected = None buffer = [] +# omp files physically begin with a fixed-width type:'title' slot line before +# the pi-shaped type:'session' header; bare pi files start with the header. +seen_title_slot = False for line in sys.stdin: line = line.strip() @@ -537,7 +545,9 @@ def handle_cursor(obj): try: obj = json.loads(line) if obj.get("type") == "session" and "cwd" in obj: - detected = "pi" + detected = "omp" if seen_title_slot else "pi" + elif obj.get("type") == "title" and len(buffer) == 1: + seen_title_slot = True elif obj.get("type") in ("user", "assistant"): detected = "claude" elif obj.get("type") in ("session_meta", "turn_context", "response_item", "event_msg"): @@ -547,7 +557,7 @@ def handle_cursor(obj): except (json.JSONDecodeError, KeyError): pass -handlers = {"claude": handle_claude, "codex": handle_codex, "cursor": handle_cursor, "pi": handle_pi} +handlers = {"claude": handle_claude, "codex": handle_codex, "cursor": handle_cursor, "pi": handle_pi, "omp": handle_pi} handler = handlers.get(detected, handle_codex) objects = [] @@ -557,7 +567,7 @@ def handle_cursor(obj): except (json.JSONDecodeError, KeyError): stats["parse_errors"] += 1 -if detected == "pi": +if detected in ("pi", "omp"): objects = _pi_context_objects(objects) for obj in objects: diff --git a/tests/fixtures/session-history/omp-session.jsonl b/tests/fixtures/session-history/omp-session.jsonl new file mode 100644 index 000000000..8638db56e --- /dev/null +++ b/tests/fixtures/session-history/omp-session.jsonl @@ -0,0 +1,11 @@ +{"type":"title","v":1,"title":"fix the auth bug in middleware","source":"auto","updatedAt":"2026-04-07T09:00:00.000Z","pad":""} +{"type":"session","version":3,"id":"test-omp-session-1","timestamp":"2026-04-07T09:00:00.000Z","cwd":"/Users/test/Code/my-repo"} +{"type":"model_change","id":"mc1","parentId":null,"timestamp":"2026-04-07T09:00:00.500Z","provider":"anthropic","modelId":"claude-sonnet-4-5"} +{"type":"thinking_level_change","id":"tl1","parentId":"mc1","timestamp":"2026-04-07T09:00:00.700Z","thinkingLevel":"medium"} +{"type":"message","id":"msg1","parentId":"tl1","timestamp":"2026-04-07T09:01:00.000Z","message":{"role":"user","content":[{"type":"text","text":"fix the auth bug in middleware"}],"timestamp":1775542860000}} +{"type":"message","id":"msg2","parentId":"msg1","timestamp":"2026-04-07T09:01:05.000Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"internal reasoning should not be extracted"},{"type":"text","text":"Let me look at the auth middleware."},{"type":"toolCall","id":"tc1","name":"read","arguments":{"path":"/Users/test/Code/my-repo/src/auth.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","stopReason":"toolUse","timestamp":1775542865000}} +{"type":"message","id":"msg3","parentId":"msg2","timestamp":"2026-04-07T09:01:06.000Z","message":{"role":"toolResult","toolCallId":"tc1","toolName":"read","content":[{"type":"text","text":"file contents here"}],"isError":false,"timestamp":1775542866000}} +{"type":"message","id":"msg4","parentId":"msg3","timestamp":"2026-04-07T09:01:10.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Found the issue. The token validation is missing the expiry check."},{"type":"toolCall","id":"tc2","name":"edit","arguments":{"path":"/Users/test/Code/my-repo/src/auth.ts","oldText":"validate()","newText":"validateToken()"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","stopReason":"toolUse","timestamp":1775542870000}} +{"type":"message","id":"msg5","parentId":"msg4","timestamp":"2026-04-07T09:01:11.000Z","message":{"role":"toolResult","toolCallId":"tc2","toolName":"edit","content":[{"type":"text","text":"String to replace not found"}],"isError":true,"timestamp":1775542871000}} +{"type":"message","id":"msg6","parentId":"msg5","timestamp":"2026-04-07T09:01:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"The first edit failed, so I will patch the middleware validation directly."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","stopReason":"stop","timestamp":1775542875000}} +{"type":"message","id":"msg7","parentId":"msg6","timestamp":"2026-04-07T09:01:20.000Z","message":{"role":"user","content":"also add a regression test for the expiry check","timestamp":1775542880000}} diff --git a/tests/session-history-scripts.test.ts b/tests/session-history-scripts.test.ts index 79fb1727a..e8b99a71b 100644 --- a/tests/session-history-scripts.test.ts +++ b/tests/session-history-scripts.test.ts @@ -313,23 +313,42 @@ describe("extract-metadata", () => { expect(session.last_ts).toContain("2026-04-07T09:01:20") }) + test("detects omp title-slot shape as omp, not pi", async () => { + // omp files physically begin with a type:'title' slot line before the + // pi-shaped session header; try_omp runs before try_pi so the file must + // not fall through to the pi detector. + const { stdout, exitCode } = await runScript("extract-metadata.py", [ + path.join(FIXTURES_DIR, "omp-session.jsonl"), + ]) + expect(exitCode).toBe(0) + const lines = parseJsonLines(stdout) + const session = lines.find((l) => !l._meta) + expect(session.platform).toBe("omp") + expect(session.cwd).toBe("/Users/test/Code/my-repo") + expect(session.session).toBe("test-omp-session-1") + expect(session.ts).toContain("2026-04-07") + expect(session.last_ts).toContain("2026-04-07T09:01:20") + }) + test("batch mode processes multiple files", async () => { const { stdout, exitCode } = await runScript("extract-metadata.py", [ path.join(FIXTURES_DIR, "claude-session.jsonl"), path.join(FIXTURES_DIR, "codex-session.jsonl"), path.join(FIXTURES_DIR, "cursor-session.jsonl"), path.join(FIXTURES_DIR, "pi-session.jsonl"), + path.join(FIXTURES_DIR, "omp-session.jsonl"), ]) expect(exitCode).toBe(0) const lines = parseJsonLines(stdout) const meta = lines.find((l) => l._meta) - expect(meta.files_processed).toBe(4) + expect(meta.files_processed).toBe(5) expect(meta.parse_errors).toBe(0) const platforms = lines.filter((l) => !l._meta).map((l) => l.platform) expect(platforms).toContain("claude") expect(platforms).toContain("codex") expect(platforms).toContain("cursor") expect(platforms).toContain("pi") + expect(platforms).toContain("omp") }) test("--cwd-filter excludes non-matching Codex sessions", async () => { @@ -987,6 +1006,28 @@ describe("extract-skeleton", () => { expect(stdout).not.toContain("file contents here") }) + test("extracts omp user, assistant, and tool messages past the title slot", async () => { + const fixture = await Bun.file( + path.join(FIXTURES_DIR, "omp-session.jsonl") + ).text() + const { stdout, exitCode } = await runScript( + "extract-skeleton.py", + [], + fixture + ) + expect(exitCode).toBe(0) + expect(stdout).toContain("[user] fix the auth bug in middleware") + expect(stdout).toContain("[assistant] Let me look at the auth middleware.") + expect(stdout).toContain("[assistant] Found the issue.") + expect(stdout).toContain("[user] also add a regression test for the expiry check") + expect(stdout).toContain("[tool] read /Users/test/Code/my-repo/src/auth.ts -> ok") + expect(stdout).toContain("[tool] edit /Users/test/Code/my-repo/src/auth.ts -> error") + expect(stdout).not.toContain("internal reasoning should not be extracted") + expect(stdout).not.toContain("file contents here") + const meta = JSON.parse(stdout.trim().split("\n").at(-1)!) + expect(meta.parse_errors).toBe(0) + }) + test("extracts Pi bashExecution commands", async () => { const lines = [ JSON.stringify({ @@ -1364,6 +1405,23 @@ describe("extract-errors", () => { expect(meta.parse_errors).toBe(0) }) + test("extracts omp tool result errors past the title slot", async () => { + const fixture = await Bun.file( + path.join(FIXTURES_DIR, "omp-session.jsonl") + ).text() + const { stdout, exitCode } = await runScript( + "extract-errors.py", + [], + fixture + ) + expect(exitCode).toBe(0) + expect(stdout).toContain("[error] tool=edit: String to replace not found") + const lines = stdout.trim().split("\n") + const meta = JSON.parse(lines[lines.length - 1]) + expect(meta.errors_found).toBe(1) + expect(meta.parse_errors).toBe(0) + }) + test("extracts Pi bashExecution errors", async () => { const lines = [ JSON.stringify({ @@ -1649,8 +1707,8 @@ describe("--output PATH mode", () => { // --------------------------------------------------------------------------- describe("auto-detection", () => { test("all supported platforms are auto-detected", async () => { - const fixtures = ["claude-session", "codex-session", "cursor-session", "pi-session"] - const expected = ["claude", "codex", "cursor", "pi"] + const fixtures = ["claude-session", "codex-session", "cursor-session", "pi-session", "omp-session"] + const expected = ["claude", "codex", "cursor", "pi", "omp"] for (let i = 0; i < fixtures.length; i++) { const fixturePath = path.join(FIXTURES_DIR, `${fixtures[i]}.jsonl`) @@ -1893,4 +1951,211 @@ describe("discover-sessions", () => { const files = stdout.trim().split("\n").filter((l) => l.trim()) expect(files).toEqual([sessionPath]) }) + + test("--platform omp discovers raw legacy buckets via the raw basename glob", async () => { + // Raw-scheme buckets ("----" here) keep the basename verbatim. A repo + // named "my repo" lands in "--Users-test-Code-my repo--", which the + // sanitized-basename glob never matches; the nonexistent --cwd means the + // exact raw probe cannot resolve, so only the raw glob can find it. + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const sessionPath = path.join( + tempHome, + ".omp/agent/sessions/--Users-test-Code-my repo--/2026-04-07T09-00-00-000Z_test.jsonl" + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + [ + "my repo", + "7", + "--cwd", + "/Users/test/Code/my repo", + "--platform", + "omp", + ], + { HOME: tempHome } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp probes the exact raw bucket for --cwd and deduplicates", async () => { + // With a resolvable --cwd, the raw home-relative bucket "-Code-my repo" is + // hit by both the exact probe and the raw basename glob; the file must be + // emitted exactly once even under single-platform invocation. + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const repoDir = path.join(tempHome, "Code", "my repo") + await fs.promises.mkdir(repoDir, { recursive: true }) + const sessionPath = path.join( + tempHome, + ".omp/agent/sessions/-Code-my repo/2026-04-07T09-00-00-000Z_test.jsonl" + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my repo", "7", "--cwd", repoDir, "--platform", "omp"], + { HOME: tempHome } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp discovers sessions under hashed bucket directories", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const bucket = + "home-my-repo-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + const sessionPath = path.join( + tempHome, + `.omp/agent/sessions/${bucket}/2026-04-07T09-00-00-000Z_test.jsonl` + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my-repo", "7", "--platform", "omp"], + { HOME: tempHome } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp discovers sessions under named-profile roots", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const bucket = + "home-my-repo-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + const sessionPath = path.join( + tempHome, + `.omp/profiles/work/agent/sessions/${bucket}/2026-04-07T09-00-00-000Z_test.jsonl` + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my-repo", "7", "--platform", "omp"], + { HOME: tempHome } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp honors PI_CODING_AGENT_SESSION_DIR flat override", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const sessionBase = fs.mkdtempSync(path.join(os.tmpdir(), "omp-sessions-")) + const sessionPath = path.join( + sessionBase, + "2026-04-07T09-00-00-000Z_test.jsonl" + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + [ + "my-repo", + "7", + "--cwd", + "/Users/test/Code/my-repo", + "--platform", + "omp", + ], + { + HOME: tempHome, + PI_CODING_AGENT_SESSION_DIR: sessionBase, + } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp honors PI_CODING_AGENT_DIR sessions subdirectory", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "omp-agent-")) + const bucket = + "home-my-repo-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + const sessionPath = path.join( + agentDir, + `sessions/${bucket}/2026-04-07T09-00-00-000Z_test.jsonl` + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my-repo", "7", "--platform", "omp"], + { HOME: tempHome, PI_CODING_AGENT_DIR: agentDir } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("--platform omp restricts to omp dirs only", async () => { + const { stdout } = await runDiscover( + ["compound-engineering-plugin", "7", "--platform", "omp"] + ) + const files = stdout.trim().split("\n").filter((l) => l.trim()) + for (const file of files) { + expect(file).toMatch(/\.omp\//) + } + }) + + test("--platform omp matches buckets whose basename omp sanitized", async () => { + // omp normalizes bucket basenames ([^a-zA-Z0-9._-]+ -> "-"), so a repo + // named "my repo" lands in a "home-my-repo-" bucket. The raw + // REPO_NAME glob would never match it. + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const bucket = + "home-my-repo-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + const sessionPath = path.join( + tempHome, + `.omp/agent/sessions/${bucket}/2026-04-07T09-00-00-000Z_test.jsonl` + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my repo", "7", "--platform", "omp"], + { HOME: tempHome } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) + + test("all-platform discovery deduplicates the shared PI_CODING_AGENT_SESSION_DIR override", async () => { + // Pi and omp both honor the flat session-dir override, so without + // deduplication each file is emitted twice (once per discoverer). + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "omp-home-")) + const sessionBase = fs.mkdtempSync(path.join(os.tmpdir(), "omp-sessions-")) + const sessionPath = path.join( + sessionBase, + "2026-04-07T09-00-00-000Z_test.jsonl" + ) + await writeFixture(sessionPath, "omp-session.jsonl") + + const { stdout, stderr, exitCode } = await runDiscover( + ["my-repo", "7", "--cwd", "/Users/test/Code/my-repo"], + { + HOME: tempHome, + PI_CODING_AGENT_SESSION_DIR: sessionBase, + } + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + const files = stdout.trim().split("\n").filter((l) => l.trim()) + expect(files).toEqual([sessionPath]) + }) }) diff --git a/tests/skills/ce-session-historian-no-skill-tool.test.ts b/tests/skills/ce-session-historian-no-skill-tool.test.ts index 51fc07e7f..aac685e55 100644 --- a/tests/skills/ce-session-historian-no-skill-tool.test.ts +++ b/tests/skills/ce-session-historian-no-skill-tool.test.ts @@ -74,3 +74,12 @@ describe("session-historian prompt no-Skill-tool regression guard", () => { expect(COMPOUND_SKILL_BODY).not.toContain(" { + test("input, synthesis, and output contracts include omp", () => { + expect(AGENT_BODY).toContain("`platform` — `claude`, `codex`, `cursor`, `pi`, or `omp`") + expect(AGENT_BODY).toContain("`cwd` — working directory when present (Codex, Pi, and omp)") + expect(AGENT_BODY).toContain("Claude Code + Codex + Cursor + Pi + omp") + expect(AGENT_BODY).toContain("[N] Pi, [N] omp") + }) +})