diff --git a/docs/extensions/reflect.md b/docs/extensions/reflect.md index 9af7c08..86f8496 100644 --- a/docs/extensions/reflect.md +++ b/docs/extensions/reflect.md @@ -1,296 +1,295 @@ -# recall-reflect PRD - -## Goal - -Define the official Reflect extension so Recall can turn local AI coding history -into a clean timeline of human intent, agent response, corrections, and -follow-up work. - -Reflect is designed as the official `recall-reflect` extension. Recall core provides session data through the stable CLI JSON/JSONL protocol; the extension owns timeline reconstruction, observed pattern prompts, and future discussion/calibration workflow. - -The primary workflow is: - -1. A user or coding agent selects a repository root or repository identity. -2. Recall core finds relevant local sessions for that project. -3. `recall-reflect` reconstructs a conversation-first timeline across those sessions. -4. `recall-reflect` surfaces observed workflow patterns as discussion prompts. -5. The user decides whether any pattern should become a workflow, skill, agent, - or instruction-file change. -6. If the user approves, the extension can prepare an explicit proposal or patch for a - later apply step. - -Reflect is not only a report. The complete product direction is an extension-led reflection and -calibration loop: inspect the timeline, discuss what the timeline means, propose -changes only after user confirmation, and later compare new sessions against -accepted changes. - -## Problem - -Recall already supports local session indexing, search, export, sharing, usage -reporting, and session-level actions. Those workflows are session-first: they -help users find, inspect, and reuse individual conversations. - -AI coding work often does not stay inside one session. A project may move from -Claude Code to Codex, from Codex to OpenCode, or from a long exploration session -to several focused follow-up sessions. Users also copy agent output into new -prompts, restart with a cleaner context, retry failed approaches, or gradually -turn repeated friction into a personal workflow. - -That makes reflection different from search. The useful unit is the project -timeline, not the individual session. A session-first summary can miss the -larger pattern: how work moved, where misunderstandings repeated, when the user -changed direction, and which behaviors might be worth preserving or changing. - -## Users - -- Power users who want to understand and improve their AI-assisted coding - workflow from local history. -- Coding agents that need a clean project timeline before proposing workflow or - skill changes. -- Maintainers who want Recall's reflection behavior to stay local-first, - explicit, and reviewable. - -## Definitions - -- **Reflect**: the official `recall-reflect` extension, a project-level workflow - that reconstructs a timeline, surfaces patterns for discussion, and can later - produce user-approved change proposals. -- **Project timeline**: a chronological narrative built from multiple sessions - that belong to the same repository or project scope. -- **Conversation-first timeline**: a timeline focused on user inputs, agent - responses, decisions, corrections, and outcomes. Low-level tool calls and raw - execution logs are supporting data, not the default narrative. -- **Observed pattern**: a repeated or notable behavior found in the timeline, - such as a handoff between tools, repeated correction, retry loop, or recurring - workflow step. -- **Discussion prompt**: a question that asks the user to confirm, reject, or - refine an observed pattern before Recall treats it as a calibration target. -- **Calibration target**: a user-confirmed pattern that may justify changing a - workflow, skill, agent instruction, project process, or instruction file. -- **Proposal**: an optional, reviewable suggestion generated from a calibration - target. A proposal is not applied unless the user explicitly approves it. - -## Scope - -### In Scope - -- Add an official reflect extension workflow scoped by repository root or - repository identity. -- Reconstruct a timeline across relevant local sessions. -- Keep the default narrative conversation-first: user messages, assistant - responses, titles, summaries, timestamps, and source/project metadata. -- Hide or summarize raw tool calls, file reads/writes, command output, and - internal events by default. -- Surface observed workflow patterns as discussion prompts instead of final - judgments. -- Support optional workflow, skill, agent, project-process, or instruction-file - proposals after user confirmation. -- Keep proposal application explicit and reviewable. -- Provide machine-readable output for agents in addition to readable terminal - output. - -### Out of Scope - -- Treating reflection as a personality profile or scorecard. -- Automatically editing skills, prompts, project files, or instruction files - without user approval. -- Uploading reflection data or sharing it remotely by default. -- Replacing `recall search`, `recall session`, `recall export`, or the TUI. -- Making low-level tool/event logs the primary report format. - -## Product Model - -Reflect has five product layers. They describe the complete extension capability; an -implementation can deliver them incrementally. - -### 1. Timeline Reconstruction - -The user-facing input should stay simple. The common command should be Recall's -extension dispatch, based on the current repository or an explicit repository -root: +# KEP: recall-reflect -```bash -recall reflect --project /path/to/repo -recall reflect --repo owner/repo -``` +## Metadata -Direct extension invocation is acceptable for testing and development: +- **Status**: Provisional +- **Type**: Feature +- **Created**: 2026-07-09 +- **Owners**: Recall maintainers +- **Extension**: `recall-reflect` +- **Stage**: phased design +- **Related design**: `docs/extensions.md` -```bash -recall-reflect --project /path/to/repo -recall-reflect --repo owner/repo +## Summary + +`recall-reflect` is the official Reflect extension for Recall. It turns local, +multi-agent AI coding history into an evidence-backed review workflow for how a +person works with coding agents. + +Reflect supports two first-class views: + +- **Personal reflection**: cross-project, cross-agent reflection over a recent + time window. +- **Project reflection**: repository-scoped reflection over a project timeline. + +The current implementation is a text/JSON project-timeline report. The next +milestone adds explicit personal and project scope semantics so Reflect can +support both cross-project personal reflection and repository-scoped continuity. +Later milestones can add an extension-owned TUI review workbench, proposal +previews, and explicit instruction-file patches. + +The long-term value loop is: + +```text +Reflect + -> actionable observations + -> evidence review + -> proposal preview + -> user approval + -> instruction patch ``` -Internally, the extension consumes indexed session metadata and messages through -Recall's stable CLI protocol with `recall export --include metadata,messages`. -Future modes may opt into usage or event records only when they need that extra -context. These inputs should not make the user-facing command feel complex. +## Motivation + +Recall already indexes local sessions from multiple coding agents and exposes +stable JSON/JSONL query surfaces. Current workflows are session-first: search, +inspect, export, share, and usage reporting. + +AI coding work is not session-first in practice. A user may plan in Claude Code, +implement in Codex, review in OpenCode, restart with a cleaner context, copy one +agent's output into another, or repeatedly correct the same behavior across +projects. + +Provider-specific reflection dashboards can only see one product's history. +Recall can reflect over mixed local history from many coding agents and show +handoffs, source roles, repeated corrections, and tool-switching friction that +single-provider tools cannot see. + +Reflect should not stop at insight. A report that only says "you often correct +scope drift" has weak user value. The product should eventually help the user +apply a confirmed lesson to future work, starting with small, previewed patches +to repository instruction files such as `AGENTS.md` or `CLAUDE.md`. -The output of this layer is a chronological project narrative. Sessions are -evidence sources, but the top-level structure is time. +### Goals -Large project histories must not be handled by sending one full timeline to an -agent or model. Reflect should build a multi-resolution timeline: compact session -or time-window summaries first, then phase summaries, then a project-level -reflection. The user can drill back into the supporting sessions when needed, but -the default report should stay within a readable and reviewable size. +- Support personal and project reflection as first-class views. +- Default to project reflection inside a Git repository. +- Allow `--personal` to force personal reflection, even inside a repository. +- Default to personal reflection outside a Git repository. +- Preserve the current text/JSON report path while scope semantics mature. +- Add a TUI-first review workbench for observations, evidence, and proposed + actions in a later milestone. +- Surface repeated friction, source roles, cross-agent handoffs, and workflow + patterns as observations, not verdicts. +- Require supporting evidence for each actionable observation. +- Apply only explicit, previewed instruction-file patches once the apply + milestone exists. +- Consume Recall data only through stable CLI JSON/JSONL output. +- Preserve machine-readable output for agents with `--format json`. -### 2. Conversation Reflection +### Non-Goals -The extension then looks for workflow patterns in the clean conversation timeline: +- Do not make Reflect a personality profile or scorecard. +- Do not rank agents as a universal leaderboard. +- Do not enforce quiet hours, usage limits, or wellbeing nudges in Recall core. +- Do not mutate Recall's SQLite database directly from the extension. +- Do not expose Rust internals or a `recall-core` library crate. +- Do not build a general-purpose patch engine for arbitrary project files. +- Do not auto-edit skills, prompts, project files, or instruction files without + user approval. +- Do not replace `recall search`, `recall session`, `recall export`, or the core + Recall TUI. -- work continuing across sessions or tools; -- agent output becoming the next prompt or direction; -- repeated corrections or redirections; -- repeated planning, debugging, review, or verification loops; -- manual multi-step workflows that recur across sessions; -- places where the user and agent appear to disagree about scope or timing. +## Proposal -These patterns should be phrased as observations and questions, not verdicts. -For example: +Add `recall-reflect` as an official extension that consumes Recall through the +stable CLI protocol and owns the reflection workflow. -```text -This timeline shows several points where implementation expanded after the user -had narrowed the scope. Is that a real workflow problem, or are these unrelated -moments? -``` +The extension reconstructs conversation-first timelines for a selected scope and +detects actionable workflow observations. The initial implementation presents +those observations in text and JSON. Later milestones add TUI evidence review, +proposal drafts, and instruction patches that apply only after explicit user +approval. -### 3. User-Guided Calibration +### User Stories -The extension should not convert every observation into a rule. The user confirms which -patterns matter. +#### Personal reflection -The calibration step turns an observed pattern into a target only after user -input: +As a user who mixes Claude Code, Codex, OpenCode, and other agents, I want to +see how I used them over the last week or month, so I can understand which +agents I use for planning, implementation, review, debugging, and cleanup. -```text -Observed pattern: scope expanded during implementation in several timeline -moments. - -What should Recall do with this? -1. Ignore it. -2. Keep it as a note for future reflection. -3. Draft a workflow change. -4. Draft an agent/skill/instruction change. -``` +#### Project reflection + +As a user starting work in a repository, I want `recall reflect` to summarize +recent project sessions and repeated friction, so I can continue work with the +right constraints in mind. -This keeps Reflect discussion-first. The extension helps surface patterns; the user -decides whether they are real, useful, or actionable. +#### Cross-agent handoff review -### 4. Proposal Generation +As a user who moves work between agents, I want Reflect to show where handoffs +worked or created friction, so I can adjust how I split tasks across tools. -After confirmation, the extension can generate reviewable proposals. Proposal types may -include: +#### Apply a lesson -- **Workflow proposal**: a new repeatable process or checkpoint. -- **Skill proposal**: a draft skill or a suggested change to an existing skill. -- **Agent behavior proposal**: a rule or behavior for an agent to follow in a - specific situation. -- **Instruction-file proposal**: a suggested change for files such as - `AGENTS.md` or `CLAUDE.md`. -- **Project-process proposal**: a project-level practice such as a checklist, - handoff note, test gate, or review habit. +As a user who repeatedly corrected an agent for scope expansion, I want Reflect +to propose a small `AGENTS.md` rule and show the exact diff before applying it, +so future agents receive the constraint earlier. -Each proposal should explain: +### Implemented Baseline -- what would change; -- why this pattern led to the proposal; -- which timeline moments are relevant; -- what the user needs to approve before anything is written. +The current `recall-reflect` implementation is intentionally smaller than the +full product direction: -### 5. Apply, Track, And Re-Reflect +- it is an official external extension binary named `recall-reflect`; +- it consumes `recall export --include metadata,messages`; +- it supports `--personal`, `--project`, `--repo`, `--source`, `--time`, + `--sync`, and `--format text|json`; +- when no project or repo is provided inside a Git repository, it infers the + current Git root as project scope; +- when no project or repo is provided outside a Git repository, it defaults to + personal scope over the recent `30d` window; +- it renders project and personal conversation timelines, chunks long sessions, + filters low-level transcript artifacts, and emits a small discussion-prompt + layer. + +The implemented baseline does not yet support a TUI output mode, proposal +persistence, or instruction-file patch application. + +### Notes And Constraints -The complete loop does not end at proposal generation. If a user accepts a -proposal, the extension should be able to track that calibration and revisit it later. +- Core remains the data plane and stable query protocol owner. +- Reflect is an external executable named `recall-reflect`. +- Reflect must not read `recall.db` directly. +- Transcript reflection should work from `metadata,messages`. +- Usage and event records are optional context for timing, token-heavy loops, or + source-role analysis. +- Progress and diagnostics go to stderr. Machine output stays on stdout. -Future reflection can then answer: +## Design Details -- What workflow or instruction change was accepted? -- Which sessions happened after the change? -- Did the same pattern disappear, continue, or change shape? -- Did the accepted change create a new kind of friction? +### Scope Resolution -This turns Reflect from a one-time summary into a long-term local feedback loop -for AI-assisted development. +Reflect supports explicit and inferred scopes: -## Command Design +1. `--personal` forces personal reflection, even inside a repository. +2. `--project ` or `--repo ` forces project reflection. +3. With no explicit scope inside a Git repository, Reflect defaults to the + current repository. +4. With no explicit scope outside a Git repository, Reflect defaults to personal + reflection over a recent time window. -### `recall reflect` +The selected scope must be explicit in TUI, text, and JSON output. Personal +reflection must not silently include all indexed history unless the user chooses +a broad time window. -Reflect on the current or selected project timeline through Recall's extension dispatch. +The implemented default personal time window is `30d`. Future releases can tune +that default if user testing shows a different recent window is more useful. + +### Command Surface ```bash recall reflect +recall reflect --personal --time 30d recall reflect --project /path/to/repo recall reflect --repo owner/repo -recall reflect --time 30d +recall reflect --personal --source codex --source claude-code --time 7d recall reflect --format json ``` Options: +- `--personal`: reflect across projects for the selected time/source scope. - `--project `: project directory boundary, including child paths. - `--repo `: repository identity such as `owner/repo` or a remote URL. -- `--time `: time window, default `all` or a - product-chosen recent default. -- `--source `: optional source filter for focused inspection. -- `--format `: default `text`. +- `--time `: time window. +- `--source `: optional source filter. Repeated values mean a + mixed-source reflection. The current implementation accepts one source; repeated + source values are a later enhancement. +- `--format `: implemented output modes. A future TUI milestone may + add `tui` and make it the default interactive mode. - `--sync`: optionally run incremental sync before reflection. -- `--include-events`: include summarized low-level events as supporting context. - -When no project or repo is provided, Recall should prefer the current repository -root when it can be resolved. If no repository can be resolved, the command -should ask the calling agent or user to choose a project scope rather than -reflecting over all history by accident. +- `--include-events`: planned option to include summarized low-level events as + supporting context. +- `--include-usage`: planned option to include usage records when available. -### `recall reflect propose` +### Future TUI Review Workbench -Draft a proposal from a selected or confirmed calibration target. +The default TUI should prioritize action over dashboard polish: -```bash -recall reflect propose --id -recall reflect propose --id --kind workflow -recall reflect propose --id --kind instruction --target AGENTS.md +```text +[ Observations ] [ Evidence ] [ Proposal / Diff ] +scope drift x4 session + moment ids AGENTS.md patch +missing tests x3 source + timestamp Apply / Dismiss +handoff friction x2 concise excerpts Save note / Draft ``` -This command prepares a proposal. It does not apply it. +The left pane is an actionable observation queue. The middle pane explains why +an observation exists. The right pane shows what the user can do with it. -### `recall reflect apply` +Supported MVP actions: -Apply a proposal after explicit user approval. +- **Dismiss**: reject the observation. +- **Save note**: keep the observation without modifying a repository. +- **Draft**: create a proposal but do not apply it. +- **Apply**: apply a previewed instruction-file patch after confirmation. -```bash -recall reflect apply --proposal --dry-run -recall reflect apply --proposal -``` +When personal reflection runs outside a repository, actions that require an +instruction patch should ask the user to choose a target repository or stay in +note/draft mode. + +### Observation Model + +Reflect should identify repeated or notable workflow patterns, including: + +- repeated scope expansion or scope-boundary reminders; +- repeated missing verification or testing reminders; +- repeated over-engineering or simplification requests; +- cross-session continuation of the same task; +- cross-agent handoffs where one source shapes the next prompt or direction; +- recurring source roles, such as exploration in one agent and implementation in + another; +- repeated planning, debugging, review, or verification loops; +- disagreement between user and agent about scope or timing. + +Each actionable observation should include: + +- stable observation id; +- concise summary; +- source roles or affected sources when relevant; +- supporting session ids and moment ids; +- representative excerpts or summaries; +- discussion prompt; +- optional proposal draft. -`--dry-run` should show the exact change. Applying should be a separate, -deliberate action. +### Future Proposal And Apply Model -## Output Design +The first apply target should be a repository instruction file: -Readable output should prefer short sections: +- `AGENTS.md`; +- `CLAUDE.md`; +- future instruction files discovered by project conventions. + +Proposal previews must show: + +- target file; +- exact diff; +- evidence moments that justify the rule; +- what the user must approve before anything is written. + +Apply must be separate and deliberate. It must never happen from observation +detection alone. + +### Text Output + +Text output should be concise and layered: ```text -Recall reflect for : - -1. Timeline -2. Observed workflow patterns -3. Discussion prompts -4. Calibration targets confirmed by the user -5. Optional proposals -6. Follow-up checks from previous calibrations +Recall reflect for : + +1. Scope summary +2. Timeline +3. Observed workflow patterns +4. Discussion prompts +5. Calibration targets confirmed by the user +6. Optional proposals +7. Follow-up checks from previous calibrations ``` -The main timeline should not read like a tool log. It should read like a concise -history of human intent and agent response. +The main timeline should read like a concise history of human intent and agent +response, not a tool log. -For long histories, output should be layered rather than exhaustive: - -1. a short project-level summary; -2. a small number of timeline phases; -3. representative moments under each phase; -4. optional drill-down commands or ids for deeper inspection. +### JSON Output JSON output should preserve enough structure for agents to continue the conversation: @@ -298,10 +297,19 @@ conversation: ```json { "scope": { + "kind": "project", "project": "/path/to/repo", "repo": "owner/repo", - "time": "30d" + "time_range": "30d", + "sources": ["codex", "claude-code"] }, + "source_roles": [ + { + "source": "claude-code", + "observed_role": "Exploration and broad planning", + "evidence_moments": ["moment-1", "moment-3"] + } + ], "timeline": [ { "id": "moment-1", @@ -324,72 +332,148 @@ conversation: } ``` -## Agent-Friendly Workflows +## Risks And Mitigations -### Reflect Before Planning +### False pattern detection -```bash -recall reflect --project /path/to/repo --time 30d --format json -# Agent summarizes the timeline and asks which pattern, if any, the user wants to calibrate. -``` +Reflect may infer a pattern from unrelated moments. -### Draft A Workflow Proposal +Mitigation: phrase findings as observations and prompts, require evidence ids, +and require user confirmation before proposals or patches. -```bash -recall reflect --project /path/to/repo -# User confirms a pattern. -recall reflect propose --id --kind workflow -``` +### Over-broad personal reflection -### Preview An Instruction Change +Personal reflection can combine private intent across projects. -```bash -recall reflect propose --id --kind instruction --target AGENTS.md -recall reflect apply --proposal --dry-run -# User reviews the exact diff before applying. -``` +Mitigation: default to a recent window, make the scope explicit, summarize by +default, and avoid broad transcript excerpts unless requested. + +### Unsafe apply behavior + +Instruction patches can change future agent behavior. + +Mitigation: MVP apply only targets instruction files, always shows a diff, and +requires explicit user approval. + +### Core boundary creep + +Reflection features could pressure Recall core to become a workflow engine. + +Mitigation: keep Reflect as an extension consuming stable CLI output. Core stays +the data plane and query protocol. + +## Graduation Criteria + +### Implemented Baseline + +- `recall-reflect` is an official extension binary with manifest support. +- Project and personal reflection work from `metadata,messages`. +- Text and JSON output render the selected scope kind, project/repo scope, + timeline phases, observed patterns, and proposal stubs. +- Low-level transcript artifacts are hidden or summarized by default. + +### Completed Milestone: Personal And Project Scopes + +- Scope resolution follows the explicit/personal/project rules above. +- `--personal` forces personal reflection, including inside a Git repository. +- `recall reflect` inside a Git repository defaults to project reflection. +- `recall reflect` outside a Git repository defaults to personal reflection over + the recent `30d` time window. +- Text and JSON output include a stable scope kind. + +### Next Milestone: Broader Reflection Signals + +- Personal reflection provides deterministic source, project, and task-shape + summaries without reading SQLite directly. -## Privacy And Safety +### Later Milestone: Interactive Review -- Reflection data comes from local session history. -- Reports should summarize transcript content. They should not paste full - transcripts unless explicitly requested. -- Low-level tool calls, file paths, command output, and internal events should - be hidden or summarized by default. -- Reflect should not automatically modify source files, project configuration, - skills, prompts, or instruction files. -- Proposal application must require explicit user approval. -- Shared or exported reflection output may contain private intent and project - context; sharing belongs behind an explicit user action. +- `recall reflect` opens the TUI by default. +- TUI shows actionable observations with evidence ids. +- TUI previews instruction-file patches. +- Apply writes only approved instruction-file patches. +- `--format json` emits machine-readable scope, observations, evidence, and + proposal stubs. -## Backward Compatibility +### Later Milestone: Persistence And Follow-Up -- Existing commands keep working: `recall search`, `recall session`, - `recall export`, `recall import`, `recall usage`, `recall share init`, and - the TUI remain unchanged. -- Existing session export schemas remain useful as internal data sources for - reflection. -- Reflect can be added without changing existing session-level workflows. +- Saved notes and draft proposals have an extension-owned persistence model. +- Cross-agent handoff detection is robust enough for mixed-source histories. +- Usage/events can enrich selected personal reflection layers without becoming + the main report. +- Follow-up reflection can compare sessions before and after accepted patches. -## Relationship To Existing Skill Workflows +### Stable -Some agent skill systems already encode behavior learned from repeated AI coding -friction. The extension should treat those systems as possible calibration targets, -not as required dependencies. +- Proposal targets beyond instruction files are intentionally selected and + covered by tests. +- Personal and project reflection have stable output semantics. +- TUI flows are covered by regression tests for scope selection, preview, apply, + and cancel paths. -For example, a user might use reflection to decide that a recurring pattern -should become a new workflow, a skill change, or an instruction-file rule. The extension -should help prepare that proposal, but the user remains responsible for deciding -whether to adopt it. +## Production Readiness Review + +### Operational impact + +Reflect runs as an external command. It should not add background services or +long-running core processes in the MVP. + +### Security and privacy + +Reflect reads local session history through Recall's CLI. It must not upload +data or share reports by default. Sensitive content should be summarized at a +high level in personal reflection. + +### Observability + +Machine output belongs on stdout. Progress, warnings, sync messages, and apply +diagnostics belong on stderr. + +### Rollback + +Instruction patches should be normal file diffs in the user's repository. Users +can review, revert, or edit them with standard Git workflows. + +## Alternatives + +### Report-only Reflect + +A report-only workflow is simpler, but it produces weaker user value. It helps +users see patterns without helping them change future agent behavior. + +### Core TUI integration first + +Embedding Reflect into Recall's core TUI would make it feel native, but it would +increase core surface area and test complexity. An extension-owned TUI keeps the +boundary cleaner. + +### Usage dashboard extension + +A usage-first dashboard would overlap with existing usage reporting. Reflect +should use usage as optional supporting context, not as the main product. + +### Agent leaderboard + +Ranking agents is easy to understand but misleading. Different sources are used +for different tasks. Reflect should describe source roles and friction instead +of declaring a universal winner. ## Open Questions -- Should `recall reflect` default to a recent time window or all indexed history? -- Should calibration targets be persisted in Recall's local database, written as - files, or only emitted in reports at first? -- Should proposal application support only instruction files first, or also - skills and project workflow documents? +- What should the default personal reflection window be outside a repository: + 7 days, 30 days, or another recent range? +- Should saved notes and draft proposals be written as extension-owned files, or + only emitted in reports at first? +- After instruction-file patches, which proposal targets should come next: + skills, project workflow documents, or personal agent preferences? - How should Reflect show optional low-level event context without turning the main report into a tool log? -- Should future TUI support show Reflect as a new dashboard, a usage tab, or a - session-adjacent view? +- Which personal reflection layers should use usage/events data, and which + should stay transcript-only? +- How should cross-agent handoffs be detected without relying on fragile source + ordering or full transcript prompts? + +## Implementation History + +- 2026-07-09: Initial Reflect design reframed as a KEP-style proposal with + personal and project views, TUI-first MVP, and instruction patch apply loop. diff --git a/extensions/recall-reflect/README.md b/extensions/recall-reflect/README.md index 4108052..506bed0 100644 --- a/extensions/recall-reflect/README.md +++ b/extensions/recall-reflect/README.md @@ -4,4 +4,8 @@ Official Recall extension for timeline-first workflow reflection. It consumes Recall through CLI JSON/JSONL output and does not read Recall's SQLite database or Rust internals. -When no `--project` or `--repo` is provided, `recall-reflect` scopes to the current git repository root. Outside a git worktree, pass `--project` or `--repo` explicitly. +When no `--project`, `--repo`, or `--personal` scope is provided, +`recall-reflect` scopes to the current git repository root inside a git +worktree. Outside a git worktree, it defaults to personal reflection over the +recent `30d` window. Pass `--personal` to force personal reflection inside a +repository. diff --git a/extensions/recall-reflect/src/main.rs b/extensions/recall-reflect/src/main.rs index 0c0da46..3f394bb 100644 --- a/extensions/recall-reflect/src/main.rs +++ b/extensions/recall-reflect/src/main.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result, bail}; use clap::{Parser, ValueEnum}; use recall_reflect::manifest; +use recall_reflect::model::ReflectScopeKind; use recall_reflect::protocol::{RecallClient, ReflectArgs}; use recall_reflect::render::render_text; use recall_reflect::report::build_reflect_report; @@ -24,6 +25,8 @@ struct Cli { #[arg(long)] repo: Option, #[arg(long)] + personal: bool, + #[arg(long)] sync: bool, } @@ -40,8 +43,17 @@ fn main() -> Result<()> { return Ok(()); } - let mut args = - ReflectArgs { source: cli.source, time: cli.time, project: cli.project, repo: cli.repo }; + let mut args = ReflectArgs { + scope_kind: if cli.personal { + ReflectScopeKind::Personal + } else { + ReflectScopeKind::Project + }, + source: cli.source, + time: cli.time, + project: cli.project, + repo: cli.repo, + }; apply_default_scope(&mut args)?; let client = RecallClient::from_env(); @@ -62,22 +74,37 @@ fn main() -> Result<()> { } fn apply_default_scope(args: &mut ReflectArgs) -> Result<()> { + if args.scope_kind == ReflectScopeKind::Personal { + if args.project.is_some() || args.repo.is_some() { + bail!("--personal cannot be combined with --project or --repo"); + } + if args.time.is_none() { + args.time = Some("30d".to_string()); + } + return Ok(()); + } + if args.project.is_some() || args.repo.is_some() { return Ok(()); } - args.project = Some(current_git_root()?); + if let Some(root) = current_git_root()? { + args.project = Some(root); + } else { + args.scope_kind = ReflectScopeKind::Personal; + args.time = Some(args.time.clone().unwrap_or_else(|| "30d".to_string())); + } Ok(()) } -fn current_git_root() -> Result { +fn current_git_root() -> Result> { let output = Command::new("git") .args(["rev-parse", "--show-toplevel"]) .output() - .context("failed to resolve the current git repository; pass --project or --repo")?; + .context("failed to resolve the current git repository")?; if !output.status.success() { - bail!("recall-reflect needs a scope outside a git worktree; pass --project or --repo"); + return Ok(None); } let root = String::from_utf8(output.stdout) @@ -86,8 +113,8 @@ fn current_git_root() -> Result { .to_string(); if root.is_empty() { - bail!("git did not return a repository root; pass --project or --repo"); + bail!("git did not return a repository root"); } - Ok(root) + Ok(Some(root)) } diff --git a/extensions/recall-reflect/src/model.rs b/extensions/recall-reflect/src/model.rs index b55914f..0347799 100644 --- a/extensions/recall-reflect/src/model.rs +++ b/extensions/recall-reflect/src/model.rs @@ -1,13 +1,42 @@ use serde::Serialize; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ReflectScopeKind { + Project, + Personal, +} + +impl ReflectScopeKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Project => "project", + Self::Personal => "personal", + } + } +} + +#[derive(Clone, Debug)] pub struct ReflectFilters { + pub scope_kind: ReflectScopeKind, pub sources: Option>, pub time_range: String, pub directory: Option, pub repo: Option, } +impl Default for ReflectFilters { + fn default() -> Self { + Self { + scope_kind: ReflectScopeKind::Project, + sources: None, + time_range: String::new(), + directory: None, + repo: None, + } + } +} + #[derive(Clone, Debug)] pub struct SourceSession { pub id: String, @@ -29,6 +58,7 @@ pub struct SourceMessage { #[derive(Debug, Clone, Serialize)] pub struct ReflectScope { + pub kind: ReflectScopeKind, pub project: Option, pub repo: Option, pub time_range: String, diff --git a/extensions/recall-reflect/src/protocol.rs b/extensions/recall-reflect/src/protocol.rs index 9dc428d..d23c966 100644 --- a/extensions/recall-reflect/src/protocol.rs +++ b/extensions/recall-reflect/src/protocol.rs @@ -4,19 +4,33 @@ use std::process::{Command, Output}; use anyhow::{Context, Result, anyhow}; use serde::Deserialize; -use crate::model::{ReflectFilters, SourceMessage, SourceSession}; +use crate::model::{ReflectFilters, ReflectScopeKind, SourceMessage, SourceSession}; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct ReflectArgs { + pub scope_kind: ReflectScopeKind, pub source: Option, pub time: Option, pub project: Option, pub repo: Option, } +impl Default for ReflectArgs { + fn default() -> Self { + Self { + scope_kind: ReflectScopeKind::Project, + source: None, + time: None, + project: None, + repo: None, + } + } +} + impl ReflectArgs { pub fn filters(&self) -> ReflectFilters { ReflectFilters { + scope_kind: self.scope_kind, sources: self.source.as_ref().map(|source| vec![source.clone()]), time_range: self.time.clone().unwrap_or_else(|| "All".to_string()), directory: self.project.clone(), diff --git a/extensions/recall-reflect/src/render.rs b/extensions/recall-reflect/src/render.rs index f59508b..5a3d7e5 100644 --- a/extensions/recall-reflect/src/render.rs +++ b/extensions/recall-reflect/src/render.rs @@ -10,6 +10,7 @@ pub fn render_text(report: &ReflectReport) -> String { let _ = writeln!(out); let _ = writeln!(out, "Scope"); + let _ = writeln!(out, " Kind: {}", report.scope.kind.as_str()); let _ = writeln!(out, " Project: {}", report.scope.project.as_deref().unwrap_or("-")); let _ = writeln!(out, " Repo: {}", report.scope.repo.as_deref().unwrap_or("-")); let _ = writeln!(out, " Time: {}", report.scope.time_range); diff --git a/extensions/recall-reflect/src/report.rs b/extensions/recall-reflect/src/report.rs index 192c882..fee7f85 100644 --- a/extensions/recall-reflect/src/report.rs +++ b/extensions/recall-reflect/src/report.rs @@ -1,8 +1,8 @@ use std::collections::{BTreeMap, HashSet}; use crate::model::{ - ConversationChunk, ReflectFilters, ReflectReport, ReflectScope, ReflectSummary, SourceSession, - TimelineMoment, TimelinePhase, + ConversationChunk, ReflectFilters, ReflectReport, ReflectScope, ReflectScopeKind, + ReflectSummary, SourceSession, TimelineMoment, TimelinePhase, }; use crate::patterns::detect_observed_patterns; @@ -13,6 +13,7 @@ pub fn build_reflect_report( filters: &ReflectFilters, ) -> ReflectReport { let scope = ReflectScope { + kind: filters.scope_kind, project: filters.directory.clone(), repo: filters.repo.clone(), time_range: if filters.time_range.is_empty() { @@ -115,7 +116,7 @@ pub fn build_reflect_report( phases.push(TimelinePhase { id: "phase-1".to_string(), - title: "Project conversation timeline".to_string(), + title: timeline_title(filters.scope_kind).to_string(), start_at, end_at, summary: format!( @@ -143,6 +144,13 @@ pub fn build_reflect_report( } } +fn timeline_title(scope_kind: ReflectScopeKind) -> &'static str { + match scope_kind { + ReflectScopeKind::Project => "Project conversation timeline", + ReflectScopeKind::Personal => "Personal conversation timeline", + } +} + fn compact_content(content: &str, max_chars: usize) -> String { let collapsed = content.split_whitespace().collect::>().join(" "); if collapsed.len() <= max_chars { @@ -313,6 +321,47 @@ mod tests { assert!(report.chunks.is_empty()); } + #[test] + fn reflect_report_includes_project_scope_kind_by_default() { + let sessions = vec![fixture_session( + "s1", + "codex", + "Scoped session", + 1000, + vec![fixture_message("user", "hello", 0, 1100)], + )]; + let filters = ReflectFilters { + directory: Some("/tmp/reflect-repo".to_string()), + ..ReflectFilters::default() + }; + + let report = build_reflect_report(sessions, &filters); + + assert_eq!(report.scope.kind.as_str(), "project"); + let json = serde_json::to_value(&report).unwrap(); + assert_eq!(json["scope"]["kind"], "project"); + } + + #[test] + fn reflect_report_labels_personal_timeline() { + let sessions = vec![fixture_session( + "s1", + "codex", + "Personal session", + 1000, + vec![fixture_message("user", "hello", 0, 1100)], + )]; + let filters = ReflectFilters { + scope_kind: crate::model::ReflectScopeKind::Personal, + time_range: "30d".to_string(), + ..ReflectFilters::default() + }; + + let report = build_reflect_report(sessions, &filters); + + assert_eq!(report.phases[0].title, "Personal conversation timeline"); + } + #[test] fn reflect_builds_timeline_across_sessions() { let sessions = vec![ diff --git a/extensions/recall-reflect/tests/reflect_cli.rs b/extensions/recall-reflect/tests/reflect_cli.rs index dede995..24e2256 100644 --- a/extensions/recall-reflect/tests/reflect_cli.rs +++ b/extensions/recall-reflect/tests/reflect_cli.rs @@ -33,6 +33,7 @@ fn reflect_cli_reads_export_jsonl_from_recall_bin() { let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(json["summary"]["sessions"], 1); assert_eq!(json["summary"]["timeline_moments"], 2); + assert_eq!(json["scope"]["kind"], "project"); assert_eq!(json["scope"]["project"], "/tmp/repo"); assert_eq!(json["scope"]["repo"], "owner/repo"); assert_eq!(json["scope"]["time_range"], "week"); @@ -98,6 +99,7 @@ fn reflect_cli_defaults_unscoped_reflection_to_current_git_root() { assert!(output.stderr.is_empty(), "stderr should be empty on success"); let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["scope"]["kind"], "project"); assert_eq!(json["scope"]["project"], repo.path().display().to_string()); let calls = fake.calls(); @@ -111,22 +113,72 @@ fn reflect_cli_defaults_unscoped_reflection_to_current_git_root() { } #[test] -fn reflect_cli_requires_explicit_scope_outside_git_worktree() { +fn reflect_cli_defaults_to_personal_scope_outside_git_worktree() { let fake = FakeRecall::new(JSONL_FIXTURE, 0, ""); let non_git = TempDir::new("recall-reflect-non-git"); let output = Command::new(env!("CARGO_BIN_EXE_recall-reflect")) .env("RECALL_BIN", fake.script_path()) .current_dir(non_git.path()) + .arg("--format") + .arg("json") + .output() + .unwrap(); + + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + assert!(output.stderr.is_empty(), "stderr should be empty on success"); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["scope"]["kind"], "personal"); + assert_eq!(json["scope"]["project"], serde_json::Value::Null); + assert_eq!(json["scope"]["repo"], serde_json::Value::Null); + assert_eq!(json["scope"]["time_range"], "30d"); + + let calls = fake.calls(); + assert_eq!(calls, ["export --limit 0 --include metadata,messages --time 30d"]); +} + +#[test] +fn reflect_cli_personal_overrides_git_root_scope() { + let fake = FakeRecall::new(JSONL_FIXTURE, 0, ""); + let repo = TempDir::new("recall-reflect-repo"); + init_git_repo(repo.path()); + + let output = Command::new(env!("CARGO_BIN_EXE_recall-reflect")) + .env("RECALL_BIN", fake.script_path()) + .current_dir(repo.path()) + .args(["--personal", "--time", "7d", "--format", "json"]) .output() .unwrap(); - assert!(!output.status.success(), "command should fail outside git without scope"); + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + assert!(output.stderr.is_empty(), "stderr should be empty on success"); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["scope"]["kind"], "personal"); + assert_eq!(json["scope"]["project"], serde_json::Value::Null); + assert_eq!(json["scope"]["time_range"], "7d"); + + let calls = fake.calls(); + assert_eq!(calls, ["export --limit 0 --include metadata,messages --time 7d"]); +} + +#[test] +fn reflect_cli_rejects_personal_with_project_or_repo_scope() { + let fake = FakeRecall::new(JSONL_FIXTURE, 0, ""); + + let output = Command::new(env!("CARGO_BIN_EXE_recall-reflect")) + .env("RECALL_BIN", fake.script_path()) + .args(["--personal", "--project", "/tmp/repo"]) + .output() + .unwrap(); + + assert!(!output.status.success(), "command should reject conflicting scope flags"); assert!(output.stdout.is_empty(), "stdout must be empty on scope errors"); let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--personal"), "stderr: {stderr}"); assert!(stderr.contains("--project") || stderr.contains("--repo"), "stderr: {stderr}"); - - assert!(fake.calls().is_empty(), "export must not run without an explicit or inferred scope"); + assert!(fake.calls().is_empty(), "export must not run for invalid scope flags"); } #[test] diff --git a/src/utils.rs b/src/utils.rs index c572157..ea2fcf1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -39,10 +39,9 @@ pub(crate) fn parse_since(s: &str) -> Option { (n, 24 * 3600 * 1000i64) } else if let Some(n) = s.strip_suffix('w') { (n, 7 * 24 * 3600 * 1000i64) - } else if let Some(n) = s.strip_suffix('m') { - (n, 30 * 24 * 3600 * 1000i64) } else { - return None; + let n = s.strip_suffix('m')?; + (n, 30 * 24 * 3600 * 1000i64) }; let n: i64 = num_str.parse().ok()?; let now = chrono::Utc::now().timestamp_millis();