Files
obelisk/references/recap-patterns.md
T
tommy0103 9fc7f202f0 feat(app): add weekly recap cards with swipeable story UI and export
Introduce a Spotify-Wrapped-style recap feature: five themed cards
  (Cover, Path, Vibe, Workflow, Closing) rendered per archetype palette,
  with keyboard/swipe navigation and image export via capture IPC. Add
  RecapList, RecapDetail, RecapExport views and recap component library.
  Wire recap:list/read/updated IPC channels through preload, document the
  retrieval-to-card contract in references/recap-patterns.md, and bundle
  dist-renderer for production use.
2026-06-14 03:16:49 +08:00

14 KiB

Obelisk Recap Patterns

Use this when Obelisk Intent Routing sends the recap intent. In practice, that means the first word after /obelisk is recap; everything after it is the recap target.

Do not self-trigger this reference from broad weekly/monthly summaries, charts, rankings, shareable-card language, or playlist-style metaphors. Those are target details only after the explicit recap intent has already selected this file.

This is an optional app handoff pattern. Obelisk stays agent-first: the agent queries sessions and memories, judges what matters, then fills card content. The app owns rendering, layout, export, and animation.

Common Period Targets

Treat the target after recap as normal user language. Common app-generated targets:

  • /obelisk recap this week -- the current calendar week in the user's timezone.
  • /obelisk recap last week -- the previous calendar week.
  • /obelisk recap this month -- the current calendar month.
  • /obelisk recap last month -- the previous calendar month.

Use the current date and timezone from the runtime/session context when available. If the target contains extra style language, keep the period meaning and treat the rest as presentation guidance.

Contract

  • Produce card content, not HTML, CSS, SVG, or layout instructions.
  • Keep the first result share-safe. Avoid secrets, tokens, private URLs, long absolute paths, raw tool outputs, and embarrassing private text unless the user asked for private analysis.
  • Concrete numbers, exact times, quotes, and verdicts need evidence. If you cannot support a detail, omit it or soften it.
  • Memory is prior notes, not ground truth. If a card uses a memory conclusion, say in prose that it came from a prior memory when answering in chat, and add memory_id evidence when producing JSON.
  • Do not propose a durable memory just because you generated a recap. Propose memory only if the retrieval reveals a reusable cross-session conclusion not already covered by memories().

Retrieval Shape

Start with orientation and bounded evidence. Use English memory queries even when the recap text will be in another language.

const period = {
  label: 'Week 24',
  after: '2026-06-08T00:00:00+08:00',
  before: '2026-06-15T00:00:00+08:00',
  timezone: 'Asia/Shanghai',
};

const map = overview({ limit: 8 });
const project = map.current.project?.project;
const scoped = project ? { project } : {};

return {
  current: map.current,
  current_project: map.current_project,
  prior_memories: memories({
    ...scoped,
    query: 'weekly recap project decisions workflows debugging shipping',
    limit: 8,
  }).map(m => ({
    id: m.id,
    path: m.path,
    anchors: m.anchors,
    session_id: m.session_id,
    created_at: m.created_at,
    summary: m.summary?.slice(0, 280),
  })),
  sessions: sessions({
    ...scoped,
    after: period.after,
    before: period.before,
    limit: 40,
  }).map(s => ({
    id: s.id,
    title: s.title,
    project: s.project,
    branch: s.git_branch,
    started_at: s.started_at,
    ended_at: s.ended_at,
    message_count: s.message_count,
  })),
  summaries: summaries({
    ...scoped,
    after: period.after,
    before: period.before,
    limit: 30,
  }).map(s => ({
    id: s.id,
    session_id: s.session_id,
    session_title: s.session_title,
    project: s.project,
    timestamp: s.timestamp,
    snippet: s.content?.slice(0, 260),
  })),
  workflows: workflows({
    ...scoped,
    after: period.after,
    before: period.before,
    limit: 20,
  }).map(w => ({
    run_id: w.run_id,
    session_id: w.session_id,
    name: w.workflow_name,
    status: w.status,
    agent_count: w.agent_count,
    tokens: w.total_tokens,
    timestamp: w.timestamp,
  })),
  failures: failures({
    ...scoped,
    after: period.after,
    before: period.before,
    limit: 12,
  }).map(f => ({
    session_id: f.session_id,
    session_title: f.session_title,
    tool: f.tool_name,
    timestamp: f.timestamp,
    snippet: f.content?.slice(0, 180),
  })),
};

Use SQL for exact aggregate numbers after the helper-first pass has established the scope. Keep meta rows out of ordinary user-visible counts.

const project = '%quiet-zero%';
const after = '2026-06-08T00:00:00+08:00';
const before = '2026-06-15T00:00:00+08:00';

const metrics = sql(`
  SELECT
    COUNT(DISTINCT s.id) AS sessions,
    COUNT(m.uuid) AS messages,
    COALESCE(SUM(COALESCE(m.input_tokens, 0) + COALESCE(m.output_tokens, 0)), 0) AS tokens,
    COUNT(DISTINCT substr(m.timestamp, 1, 10)) AS active_days
  FROM sessions s
  LEFT JOIN messages m ON m.session_id = s.id
  WHERE s.project LIKE ?
    AND COALESCE(m.is_meta, 0) = 0
    AND m.timestamp >= ?
    AND m.timestamp < ?
`, project, after, before)[0];

const user_messages = sql(`
  SELECT
    m.uuid,
    m.session_id,
    s.title AS session_title,
    m.timestamp,
    substr(m.text, 1, 500) AS text
  FROM messages m
  JOIN sessions s ON s.id = m.session_id
  WHERE s.project LIKE ?
    AND m.type = 'user'
    AND m.content_type = 'text'
    AND COALESCE(m.is_meta, 0) = 0
    AND m.timestamp >= ?
    AND m.timestamp < ?
  ORDER BY m.timestamp
  LIMIT 300
`, project, after, before);

return { metrics, user_messages };

Archetypes

Choose one dominant archetype from the evidence. Do not force all cards to match it; the archetype is the cover persona and tone baseline.

archetype essential action tone baseline
architect establishes structure, boundaries, and systems from above matter-of-fact, structural pride
debugger loops through symptoms until root cause becomes visible wry, weary, occasional dark humor
shipper keeps pushing forward with dense cadence energetic, slightly breathless
curator collects, organizes, refines, and distills reflective, low-key clarity
director coordinates many threads from the center outward observant, slight remove
cartographer reorganizes known structure, redraws maps, moves boundaries patient, surveyor-like
wanderer crosses projects without one obvious center, leaving traces gentle, accepting, no apology

Selection hints:

  • Use architect when the week is dominated by system design, APIs, schemas, boundaries, or core concepts.
  • Use debugger when repeated failures, false positives, regressions, or root cause hunts dominate.
  • Use shipper when the evidence shows sustained implementation velocity, builds, releases, or many completed edits.
  • Use curator when the work is mostly cleanup, documentation, memory, organization, or taste/refinement.
  • Use director when workflows, subagents, review loops, or multi-agent coordination are the main story.
  • Use cartographer when the user moves modules, redraws information architecture, re-scopes boundaries, or turns "this lives here" into "this belongs there".
  • Use wanderer when the period spans many unrelated projects and the honest story is exploratory rather than centered.

JSON Shape

The schema is deliberately card-content oriented. Keep keys stable, but let strings carry the style. Card text can use the user's language; field names stay English.

For weekly recaps, metrics.active_days and cover activity should contain 7 numbers ordered Monday through Sunday, where 0 means no visible activity and 1 means active. For monthly recaps, use one value per calendar day in the period, or omit the field if the evidence is too thin.

type Recap = {
  schema_version: "obelisk.recap.v1";
  kind: "weekly" | "monthly";
  generated_at: string;

  period: {
    label: string;
    start: string;
    end: string;
    timezone: string;
  };

  source: {
    project?: string;
    session_ids: string[];
    memory_ids?: string[];
  };

  metrics: {
    sessions?: number;
    messages?: number;
    tokens?: number;
    active_days?: number[];
    streak_days?: number;
    workflows?: number;
    workflow_agents?: number;
    corrections?: number;
  };

  persona: {
    archetype:
      | "architect"
      | "debugger"
      | "shipper"
      | "curator"
      | "director"
      | "cartographer"
      | "wanderer";
    title: string;
    subtitle: string;
    tone: string;
  };

  cards: [
    CoverCard,
    ThinkingPathCard,
    VibeCard,
    WorkflowOrToolsCard,
    ClosingCard
  ];

  evidence?: Evidence[];
};

Card 1: Cover

type CoverCard = {
  type: "cover";
  badge: string;
  title: string;
  subtitle: string;
  activity: number[];
  footer: string;
  evidence_refs?: string[];
};

Use the cover to name the period and the persona. The title can be an archetype label such as "The Architect"; the subtitle should summarize the period's real dominant work in one line.

Card 2: Thinking Path

type ThinkingPathCard = {
  type: "thinking_path";
  title: string;
  items: Array<{
    day: string;
    prompt: string;
    outcome: string;
    evidence_refs?: string[];
  }>;
};

Pick 3-6 turning points. A prompt is the question, friction, or task that started the path. An outcome is the decision, fix, or learned framing.

Card 3: Vibe

type VibeCard = {
  type: "vibe";
  title: string;
  observations: Array<{
    label: string;
    text: string;
    count?: number;
    time?: string;
    evidence_refs?: string[];
  }>;
  meter?: {
    label: string;
    value: number;
    caption: string;
  };
  quote?: {
    text: string;
    caption?: string;
    evidence_refs?: string[];
  };
};

This card can be playful, but it must stay grounded. Repeated phrases should be counted from visible user messages, not meta messages or tool results.

Card 4: Workflows Or Tools

type WorkflowOrToolsCard = {
  type: "workflow" | "tool_habits" | "debugging" | "shipping";
  title: string;
  summary?: string;
  stats?: string;
  items: Array<{
    name: string;
    outcome: string;
    evidence_refs?: string[];
  }>;
  verdict: string;
};

Choose the card type that best fits the period. Use workflow when workflow runs or subagents are the story; debugging when failures and fixes dominate; shipping when completed implementation dominates; tool_habits when the period is mostly about how the user worked.

Card 5: Closing

type ClosingCard = {
  type: "closing";
  headline: string;
  stats: string[];
  most_said_phrase?: string;
  signoff: string;
  evidence_refs?: string[];
};

Close with one high-signal stat and a short signoff. Avoid turning the closing card into a second summary.

Evidence

type Evidence = {
  id: string;
  session_id?: string;
  message_uuid?: string;
  memory_id?: string;
  summary?: string;
};

Evidence is for local traceability and app inspection. It does not have to be shown on exported cards. Prefer short summaries over raw snippets.

Output Rules

  • If the user asks for app handoff, return one JSON object and no surrounding prose.
  • If the user asks conversationally, answer with the card content naturally and include JSON only if useful.
  • Keep cards.length === 5 in the order above.
  • Do not include unsupported cards just to fill space. It is better to make a quieter card than to invent drama.
  • Do not include raw SQL, query scripts, or private evidence in the shareable card text.

Minimal Example

{
  "schema_version": "obelisk.recap.v1",
  "kind": "weekly",
  "generated_at": "2026-06-13T03:40:00+08:00",
  "period": {
    "label": "Week 24",
    "start": "2026-06-08",
    "end": "2026-06-14",
    "timezone": "Asia/Shanghai"
  },
  "source": {
    "project": "quiet-zero",
    "session_ids": ["sid-a", "sid-b"],
    "memory_ids": ["mem-a"]
  },
  "metrics": {
    "sessions": 12,
    "messages": 847,
    "tokens": 2400000,
    "active_days": [1, 1, 1, 0, 1, 1, 1],
    "workflows": 3,
    "workflow_agents": 42
  },
  "persona": {
    "archetype": "architect",
    "title": "The Architect",
    "subtitle": "Designed a memory system from raw sessions to durable notes.",
    "tone": "matter-of-fact, structural pride"
  },
  "cards": [
    {
      "type": "cover",
      "badge": "Week 24",
      "title": "The Architect",
      "subtitle": "Designed a full memory layer without turning sessions into a wiki.",
      "activity": [1, 1, 1, 0, 1, 1, 1],
      "footer": "12 sessions - 2.4M tokens",
      "evidence_refs": ["e1", "e2"]
    },
    {
      "type": "thinking_path",
      "title": "Your thinking path",
      "items": [
        {
          "day": "Mon",
          "prompt": "Why compile sessions into a wiki?",
          "outcome": "Kept raw SQLite as the evidence layer.",
          "evidence_refs": ["e1"]
        }
      ]
    },
    {
      "type": "vibe",
      "title": "Your vibe this week",
      "observations": [
        {
          "label": "Catchphrase",
          "text": "This is too ugly.",
          "count": 4,
          "evidence_refs": ["e3"]
        }
      ],
      "meter": {
        "label": "Patience",
        "value": 0.8,
        "caption": "saint"
      }
    },
    {
      "type": "workflow",
      "title": "Are you a Workflow Enjoyer?",
      "stats": "3 workflows - 42 agents",
      "items": [
        {
          "name": "hono-plugin-review",
          "outcome": "perfect",
          "evidence_refs": ["e4"]
        }
      ],
      "verdict": "Mostly tolerated"
    },
    {
      "type": "closing",
      "headline": "19 day streak",
      "stats": ["847 messages exchanged"],
      "most_said_phrase": "Okay, start doing it.",
      "signoff": "See you next week.",
      "evidence_refs": ["e5"]
    }
  ],
  "evidence": [
    {
      "id": "e1",
      "session_id": "sid-a",
      "message_uuid": "msg-a",
      "summary": "The user chose raw SQLite as the evidence layer."
    }
  ]
}