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

509 lines
14 KiB
Markdown

# 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.
```js
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.
```js
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.
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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
```json
{
"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."
}
]
}
```