Browse Source

Merge pull request #633 from mhenke/feature/reflect-global

Alvin 1 month ago
parent
commit
b7f3682818

+ 191 - 0
docs/adr/001-session-reflection-mode.md

@@ -0,0 +1,191 @@
+# ADR-001: Session Reflection Mode for /reflect
+
+**Date:** 2026-06-29
+**Status:** Accepted
+**Deciders:** User + Orchestrator
+
+## Context
+
+We are adding a `--sessions` mode to the `/reflect` command in oh-my-opencode-slim. This mode enables cross-session reflection by analyzing past OpenCode sessions to find repeated patterns, friction, and improvement opportunities.
+
+Current `/reflect` only looks at the current conversation and project files. The new mode analyzes historical sessions across all repos to find patterns like:
+- "Which workflows succeed most often?"
+- "Which agents get stuck?"
+- "Which models cause retries?"
+- "What precedes successful completions?"
+
+## Decisions
+
+### 1. Implementation Approach: Prompt-only
+
+**Decision:** Extend the reflect skill (SKILL.md) with instructions for session reflection. No new code tools or command hooks.
+
+**Rationale:**
+- Consistent with how `/reflect` currently works (prompt-based guidance)
+- The LLM already has tools (Read, Write, Bash) to do everything needed
+- Smallest useful form — no code changes required
+- YAGNI: Start simple, add code if prompt-only proves insufficient
+
+**Alternatives considered:**
+- Hybrid (skill + command hook): More reliable enumeration, but adds code complexity
+- Full code (new tool): Fastest execution, but most complex
+
+### 2. Command Syntax
+
+**Decision:** Remove `--global` (never shipped), add `--sessions` flag.
+
+```
+/reflect                          # Local mode (current repo)
+/reflect release workflow         # Local mode, focused theme
+/reflect --sessions               # Session archaeology (last 50 sessions)
+/reflect --sessions --last 20     # Session archaeology, last 20
+/reflect --sessions release workflow  # Session archaeology, focused theme
+```
+
+**Rationale:**
+- `--global` was never rolled out, so no migration needed
+- `--sessions` clearly describes the mode's purpose
+- `--last N` provides user control over scope
+
+### 3. Session Discovery
+
+**Decision:** LLM reads `~/.local/share/opencode/log/opencode.log` and greps for `session.id=ses_[a-f0-9]+` to extract session IDs.
+
+**Rationale:**
+- Session IDs are reliably present in the main OpenCode log
+- Pattern is stable and parseable with simple grep
+- No new API dependencies needed
+
+**Log format:**
+```
+timestamp=2026-06-10T15:08:45.427Z level=INFO run=9bd29194 message=loop session.id=ses_14de9c68effegtZtlATm42wnz7 step=0
+```
+
+### 4. Session Scope
+
+**Decision:** Analyze last N sessions total (regardless of project), not per-project.
+
+**Rationale:**
+- Simpler to implement
+- Patterns emerge naturally across repos
+- User controls scope with `--last N`
+- Per-project caps can be added later if needed
+
+**Default:** 50 sessions
+**Maximum:** 100 sessions (cap to avoid context explosion)
+
+### 5. Storage Location
+
+**Decision:** Store reflection summaries in `~/.config/opencode/oh-my-opencode-slim/reflections/`.
+
+**Rationale:**
+- Existing OMOS directory already contains presets, prompts, orchestrator_append.md
+- Pragmatic: keeps all OMOS data in one place
+- Easy discovery and cleanup
+- Global across projects (sessions are not project-specific)
+
+**Structure:**
+```
+~/.config/opencode/oh-my-opencode-slim/reflections/
+  sessions/
+    ses_14de9c68effegtZtlATm42wnz7.json
+  weekly/
+    week-26.json
+  monthly/
+    month-06.json
+```
+
+**Alternatives considered:**
+| Option | Pros | Cons | Decision |
+|--------|------|------|----------|
+| `~/.config/opencode/oh-my-opencode-slim/reflections/` | Existing directory, single location | Mixes config and data | **Accepted** |
+| `~/.local/share/oh-my-opencode-slim/reflections/` | XDG-compliant, data separation | New directory, splits OMOS data | Rejected |
+| `.slim/reflections/` (project-local) | Tied to codebase | Sessions are global, not project-specific | Rejected |
+| Ephemeral (no storage) | No disk overhead | Re-analyzes expensive sessions, no trend tracking | Rejected |
+
+### 6. Two-Phase Architecture
+
+**Decision:** Per-session reflection first, then aggregation.
+
+**Rationale:**
+- Scalable: processes one session at a time (not hundreds in context)
+- Aggregation works on concise summaries (20-30k tokens) not raw sessions (millions)
+- Enables hierarchical aggregation: session → weekly → monthly
+
+**Flow:**
+```
+OpenCode logs
+  → Extract session IDs
+  → For each session:
+      → Load via client.session.messages()
+      → Analyze and produce structured summary
+      → Store in reflections/sessions/<id>.json
+  → Aggregate all summaries
+  → Produce final recommendations
+```
+
+### 7. Cache Pattern
+
+**Decision:** LLM manages its own cache using Read/Write tools.
+
+**Rationale:**
+- No new code needed — LLM already has file tools
+- Avoids re-analyzing expensive sessions
+- Enables incremental updates (only analyze new sessions)
+
+**Logic:**
+1. Check if `reflections/sessions/<id>.json` exists
+2. If yes, load it (saves tokens)
+3. If no, analyze session and save summary
+4. Aggregate across all summaries for final report
+
+### 8. Per-Session Analysis
+
+**Decision:** Each session produces a structured JSON summary with metadata, frictions, and recommendations.
+
+**Schema:**
+```json
+{
+  "session": "ses_14de9c68effegtZtlATm42wnz7",
+  "project": "/home/user/Projects/oh-my-opencode-slim",
+  "timestamp": "2026-06-10T15:08:45.427Z",
+  "goal": "Fix CI failure",
+  "success": true,
+  "frictions": [
+    "Repeated grep to find test file",
+    "Three failed test runs before passing"
+  ],
+  "recommendations": [
+    "Create /test-ci command"
+  ],
+  "duration_minutes": 18,
+  "models_used": ["opencode/mimo-v2.5-free"],
+  "agents_used": ["orchestrator", "fixer", "explorer"],
+  "tools_used": ["Read", "Edit", "Bash"],
+  "confidence": 0.85
+}
+```
+
+**Confidence scoring:**
+- 0.9-1.0: Clear success/failure, obvious patterns
+- 0.7-0.9: Likely outcome, patterns inferred from tool usage
+- 0.5-0.7: Uncertain outcome, limited evidence
+- <0.5: Skip or mark as "needs more evidence"
+
+## Implementation Notes
+
+- The LLM manages its own cache using Read/Write tools
+- Reflection files are JSON with session metadata, frictions, and recommendations
+- Hierarchical aggregation: session → weekly → monthly summaries
+- Old reflections can be pruned by age or count (configurable)
+- Skill instructions guide the LLM through the full workflow
+
+## Consequences
+
+- No code changes needed — purely skill instruction updates
+- All OMOS persistent data lives in one directory tree
+- Reflections are available across all projects (global)
+- LLM manages file I/O, cache, and aggregation
+- Users can inspect or delete reflections manually
+- Hierarchical aggregation (weekly/monthly) is possible via stored summaries
+- Per-project session caps can be added later if needed

+ 102 - 0
src/hooks/reflect/index.test.ts

@@ -81,6 +81,108 @@ describe('reflect command hook', () => {
     expect(output.parts[0].text).toContain('MCP/tool permission change');
   });
 
+  test('detects --sessions flag and activates session mode', async () => {
+    const hook = createReflectCommandHook();
+    hook.registerCommand({});
+    const output = { parts: [{ type: 'text', text: 'template' }] };
+
+    await hook.handleCommandExecuteBefore(
+      { command: 'reflect', sessionID: 's1', arguments: '--sessions' },
+      output,
+    );
+
+    expect(output.parts).toHaveLength(1);
+    expect(output.parts[0].text).toContain('Session Reflection Mode:');
+    expect(output.parts[0].text).toContain('Analyze the last 50 sessions');
+    expect(output.parts[0].text).toContain(
+      '- Extract session IDs from OpenCode logs',
+    );
+    expect(output.parts[0].text).toContain(
+      '- Load session content from SQLite database',
+    );
+    expect(output.parts[0].text).toContain(
+      '- Analyze each session for patterns and friction',
+    );
+    expect(output.parts[0].text).toContain(
+      '- Aggregate findings across all sessions',
+    );
+    expect(output.parts[0].text).toContain(
+      '- Report with scope (global/cross-repo/project-specific), confidence, and impact',
+    );
+    // Default focus for session mode
+    expect(output.parts[0].text).toContain(
+      'Analyze recent sessions to find repeated patterns, friction, and improvement opportunities.',
+    );
+  });
+
+  test('parses --last N flag in session mode', async () => {
+    const hook = createReflectCommandHook();
+    hook.registerCommand({});
+    const output = { parts: [{ type: 'text', text: 'template' }] };
+
+    await hook.handleCommandExecuteBefore(
+      {
+        command: 'reflect',
+        sessionID: 's1',
+        arguments: '--sessions --last 20',
+      },
+      output,
+    );
+
+    expect(output.parts).toHaveLength(1);
+    expect(output.parts[0].text).toContain('Analyze the last 20 sessions');
+  });
+
+  test('caps --last at 100 in session mode', async () => {
+    const hook = createReflectCommandHook();
+    hook.registerCommand({});
+    const output = { parts: [{ type: 'text', text: 'template' }] };
+
+    await hook.handleCommandExecuteBefore(
+      {
+        command: 'reflect',
+        sessionID: 's1',
+        arguments: '--sessions --last 999',
+      },
+      output,
+    );
+
+    expect(output.parts).toHaveLength(1);
+    expect(output.parts[0].text).toContain('Analyze the last 100 sessions');
+  });
+
+  test('--sessions with focus text includes both session mode and custom focus', async () => {
+    const hook = createReflectCommandHook();
+    hook.registerCommand({});
+    const output = { parts: [{ type: 'text', text: 'template' }] };
+
+    await hook.handleCommandExecuteBefore(
+      {
+        command: 'reflect',
+        sessionID: 's1',
+        arguments: '--sessions feedback on PR reviews',
+      },
+      output,
+    );
+
+    expect(output.parts).toHaveLength(1);
+    expect(output.parts[0].text).toContain('Session Reflection Mode:');
+    expect(output.parts[0].text).toContain('Focus:\nfeedback on PR reviews');
+  });
+
+  test('defaults to 50 sessions when --last is not provided', async () => {
+    const hook = createReflectCommandHook();
+    hook.registerCommand({});
+    const output = { parts: [{ type: 'text', text: 'template' }] };
+
+    await hook.handleCommandExecuteBefore(
+      { command: 'reflect', sessionID: 's1', arguments: '--sessions' },
+      output,
+    );
+
+    expect(output.parts[0].text).toContain('Analyze the last 50 sessions');
+  });
+
   test('ignores other commands', async () => {
     const hook = createReflectCommandHook();
     hook.registerCommand({});

+ 34 - 3
src/hooks/reflect/index.ts

@@ -1,13 +1,32 @@
 const COMMAND_NAME = 'reflect';
 
-function activationPrompt(focus: string): string {
+function activationPrompt(
+  focus: string,
+  isSessionMode = false,
+  lastN = 50,
+): string {
   const focusBlock = focus
     ? ['Focus:', focus]
     : [
         'Focus:',
-        'Review recent work broadly and identify repeated workflow friction worth improving.',
+        isSessionMode
+          ? 'Analyze recent sessions to find repeated patterns, friction, and improvement opportunities.'
+          : 'Review recent work broadly and identify repeated workflow friction worth improving.',
       ];
 
+  const modeBlock = isSessionMode
+    ? [
+        '',
+        'Session Reflection Mode:',
+        `- Analyze the last ${lastN} sessions (use --last N to adjust)`,
+        '- Extract session IDs from OpenCode logs',
+        '- Load session content from SQLite database',
+        '- Analyze each session for patterns and friction',
+        '- Aggregate findings across all sessions',
+        '- Report with scope (global/cross-repo/project-specific), confidence, and impact',
+      ]
+    : [];
+
   return [
     'Use the reflect skill for this request.',
     '',
@@ -19,6 +38,7 @@ function activationPrompt(focus: string): string {
     '- treat creating nothing as a valid result when evidence is weak;',
     '- ask before changing prompts, skills, commands, agents, MCP access, or config unless the user explicitly requested the exact edit;',
     '- return a compact report with findings, recommended changes, skipped candidates, and items needing more evidence.',
+    ...modeBlock,
     '',
     ...focusBlock,
   ].join('\n');
@@ -54,10 +74,21 @@ export function createReflectCommandHook(): {
     handleCommandExecuteBefore: async (input, output) => {
       if (input.command !== COMMAND_NAME || !shouldHandleCommand) return;
 
+      const args = input.arguments.trim();
+      const isSessionMode = args.includes('--sessions');
+      const lastMatch = args.match(/--last\s+(\d+)/);
+      const last = lastMatch ? Math.min(parseInt(lastMatch[1], 10), 100) : 50;
+
+      // Remove flags from focus text
+      const focus = args
+        .replace(/--sessions/g, '')
+        .replace(/--last\s+\d+/g, '')
+        .trim();
+
       output.parts.length = 0;
       output.parts.push({
         type: 'text',
-        text: activationPrompt(input.arguments.trim()),
+        text: activationPrompt(focus, isSessionMode, last),
       });
     },
   };

+ 133 - 0
src/skills/reflect/SKILL.md

@@ -17,6 +17,7 @@ The goal is to identify real repeated friction and suggest practical improvement
 Use Reflect when the user asks to:
 
 - run `/reflect` or `/reflect <focus>`;
+- run `/reflect --sessions` for session archaeology;
 - learn from recent sessions or repeated workflows;
 - find work they keep doing manually;
 - improve their oh-my-opencode-slim setup based on actual usage using oh-my-opencode-slim skill;
@@ -26,6 +27,136 @@ Use Reflect when the user asks to:
 Do not use Reflect for ordinary implementation work, one-off debugging, broad
 architecture review, or speculative agent creation without workflow evidence.
 
+## Session Mode
+
+When the user includes `--sessions` in their reflect command, shift to session
+archaeology: analyze historical OpenCode sessions across all repos to find
+repeated patterns, friction, and improvement opportunities.
+
+### Session Discovery
+
+1. **Load recent sessions** — Query the SQLite database directly:
+   ```bash
+   bun -e "import Database from 'bun:sqlite'; const db = new Database('/home/mhenke/.local/share/opencode/opencode.db'); console.log(db.query('SELECT id, directory, title, agent, model, time_created, cost, tokens_input, tokens_output FROM session ORDER BY time_created DESC LIMIT 50').all())"
+   ```
+   Adjust `LIMIT 50` to `--last N` if specified.
+
+   **Session table columns:** `id, directory, title, agent, model, time_created, cost, tokens_input, tokens_output`
+
+2. **Load session messages** — For each session ID, query the message table:
+   ```bash
+   bun -e "import Database from 'bun:sqlite'; const db = new Database('/home/mhenke/.local/share/opencode/opencode.db'); console.log(db.query('SELECT data FROM message WHERE session_id = ?').all('ses_14de9c68effegtZtlATm42wnz7'))"
+   ```
+
+   **Message table columns:** `id, session_id, time_created, time_updated, data` (data is JSON with role, agent, model, summary, etc.)
+
+### Per-Session Analysis
+
+For each session, analyze and produce a structured summary:
+
+```json
+{
+  "session": "ses_14de9c68effegtZtlATm42wnz7",
+  "project": "/home/user/Projects/oh-my-opencode-slim",
+  "timestamp": "2026-06-10T15:08:45.427Z",
+  "goal": "Fix CI failure",
+  "success": true,
+  "frictions": [
+    "Repeated grep to find test file",
+    "Three failed test runs before passing"
+  ],
+  "recommendations": [
+    "Create /test-ci command"
+  ],
+  "duration_minutes": 18,
+  "models_used": ["opencode/mimo-v2.5-free"],
+  "agents_used": ["orchestrator", "fixer", "explorer"],
+  "tools_used": ["Read", "Edit", "Bash"],
+  "confidence": 0.85
+}
+```
+
+**Confidence scoring:**
+- 0.9-1.0: Clear success/failure, obvious patterns
+- 0.7-0.9: Likely outcome, patterns inferred from tool usage
+- 0.5-0.7: Uncertain outcome, limited evidence
+- <0.5: Skip or mark as "needs more evidence"
+
+### Storage and Caching
+
+Store session summaries in `~/.config/opencode/oh-my-opencode-slim/reflections/sessions/`.
+
+**Cache logic:**
+1. Check if `<session-id>.json` exists in reflections directory
+2. If yes, load it (saves tokens)
+3. If no, analyze session and save summary
+4. Aggregate across all summaries for final report
+
+### Aggregation
+
+After analyzing all sessions, aggregate findings:
+
+1. **Group by theme** — sessions with similar frictions cluster together
+2. **Count frequency** — "42/50 sessions had repeated grep before editing"
+3. **Rank by impact** — prioritize recommendations that appear most often
+4. **Filter noise** — skip one-off issues, focus on repeated patterns
+5. **Cross-reference** — see if patterns correlate with specific models, agents, or repos
+
+**Scope categories:**
+- **Global** — applies to all repos (pattern seen in >50% of repos)
+- **Cross-repo** — applies to specific repos where pattern appears
+- **Project-specific** — only relevant to one repo
+
+### Output Format
+
+Return a compact report with scope and confidence:
+
+```text
+Session Reflection Report
+Analyzing 50 most recent sessions across 8 repos.
+
+Repos analyzed:
+- <repo> (<N> sessions)
+- ... (M more)
+
+Findings
+- <pattern>: N/50 sessions across M repos.
+  - Scope: global | cross-repo (<repos>) | project-specific (<repo>)
+  - Confidence: 0.95
+  - Impact: High | Medium | Low
+
+Recommended changes
+- <asset>: <purpose>
+  - Scope: global | cross-repo (<repos>) | project-specific (<repo>)
+  - Confidence: 0.97
+  - Estimated time saved: High | Medium | Low
+
+Skipped
+- <candidate>: why not worth packaging now.
+  - Scope: <reason>
+  - Confidence: <score>
+
+Needs more evidence
+- <candidate>: what would make it actionable.
+  - Current scope: <what we've seen>
+  - Required scope: <what would confirm>
+```
+
+### Error Handling
+
+**Log file issues:**
+- Log doesn't exist → "No OpenCode log found at <path>. Run OpenCode in at least one repo first."
+- Log is empty → "OpenCode log is empty. No sessions to analyze."
+
+**Session loading issues:**
+- Session ID not loadable → Skip with warning: "Session <id> could not be loaded, skipping."
+- Session has no messages → Skip: "Session <id> has no messages."
+
+**Recovery pattern:**
+- Log the failure
+- Continue with remaining sessions
+- Report failures at end: "3 sessions skipped due to load errors"
+
 ## Core Contract
 
 Reflect must be conservative and evidence-driven.
@@ -66,6 +197,8 @@ Reflect can be triggered directly:
 ```text
 /reflect
 /reflect release workflow and checks
+/reflect --sessions
+/reflect --sessions --last 100
 ```
 
 With no arguments, review recent work broadly. With arguments, focus the review