personal memory agent
1# solstone App Development Guide
2
3**Complete guide for building apps in the `apps/` directory.**
4
5Apps are the primary way to extend solstone's web interface (Convey). Each app is a self-contained module discovered automatically using **convention over configuration**—no base classes or manual registration required.
6
7> **How to use this document:** This guide serves as a catalog of patterns and references. Each section points to authoritative source files—read those files alongside this guide for complete details. When in doubt, the source code is the definitive reference.
8
9---
10
11## Quick Start
12
13Create a minimal app in two steps:
14
15```bash
16# 1. Create app directory (use underscores, not hyphens!)
17mkdir apps/my_app
18
19# 2. Create workspace template
20touch apps/my_app/workspace.html
21```
22
23**Minimal `workspace.html`:**
24```html
25<h1>Hello from My App!</h1>
26```
27
28**That's it!** Restart Convey and your app is automatically available at `/app/my_app`.
29
30All apps are served via a shared route handler at `/app/{app_name}`. You only need `routes.py` if your app requires custom routes beyond the index page (e.g., API endpoints, form handlers, or navigation routes).
31
32---
33
34## Directory Structure
35
36```
37apps/my_app/
38├── workspace.html # Required: Main content template
39├── routes.py # Optional: Flask blueprint (only if custom routes needed)
40├── tools.py # Optional: App tool functions for agent workflows
41├── call.py # Optional: CLI commands via Typer (auto-discovered)
42├── events.py # Optional: Server-side event handlers (auto-discovered)
43├── app.json # Optional: Metadata (icon, label, facet support)
44├── app_bar.html # Optional: Bottom bar controls (forms, buttons)
45├── background.html # Optional: Background JavaScript service
46├── talent/ # Optional: Custom agents, generators, and skills (auto-discovered)
47│ └── my-skill/ # Optional: Agent Skill directories (SKILL.md + resources)
48├── maint/ # Optional: One-time maintenance tasks (auto-discovered)
49└── tests/ # Optional: App-specific tests (run via make test-apps)
50```
51
52### File Purposes
53
54| File | Required | Purpose |
55|------|----------|---------|
56| `workspace.html` | **Yes** | Main app content (rendered in container) |
57| `routes.py` | No | Flask blueprint for custom routes (API endpoints, forms, etc.) |
58| `tools.py` | No | Callable tool functions for AI agent workflows |
59| `call.py` | No | CLI commands via Typer, accessed as `sol call <app>` (auto-discovered) |
60| `events.py` | No | Server-side Callosum event handlers (auto-discovered) |
61| `app.json` | No | Icon, label, facet support overrides |
62| `app_bar.html` | No | Bottom fixed bar for app controls |
63| `background.html` | No | Background service (WebSocket listeners) |
64| `talent/` | No | Custom agents, generators, and skills (`.md` files + skill subdirectories) |
65| `maint/` | No | One-time maintenance tasks (run on Convey startup) |
66| `tests/` | No | App-specific tests with self-contained fixtures |
67
68---
69
70## Naming Conventions
71
72**Critical for auto-discovery:**
73
741. **App directory**: Use `snake_case` (e.g., `my_app`, **not** `my-app`)
752. **Blueprint variable** (if using routes.py): Must be `{app_name}_bp` (e.g., `my_app_bp`)
763. **Blueprint name** (if using routes.py): Must be `app:{app_name}` (e.g., `"app:my_app"`)
774. **URL prefix**: Convention is `/app/{app_name}` (e.g., `/app/my_app`)
78
79**Index route**: All apps are automatically served at `/app/{app_name}` via a shared handler. You don't need to define an index route in `routes.py`.
80
81See `apps/__init__.py` for discovery logic and route injection.
82
83---
84
85## Required Files
86
87### 1. `workspace.html` - Main Content
88
89The workspace template is included inside the app container (`app.html`).
90
91**Available Template Context:**
92- `app` - Current app name (auto-injected from URL)
93- `day` - Current day as YYYYMMDD string (auto-injected from URL for apps with `date_nav: true`)
94- `facets` - List of active facet dicts: `[{name, title, color, emoji}, ...]`
95- `selected_facet` - Currently selected facet name (string or None)
96- `app_registry` - Registry with all apps (usually not needed directly)
97- `state.journal_root` - Path to journal directory
98- Any variables passed from route handler via `render_template(...)`
99
100**Note:** The server-side `selected_facet` is also available client-side as `window.selectedFacet` (see JavaScript APIs below).
101
102**Vendor Libraries:**
103- Use `{{ vendor_lib('marked') }}` for markdown rendering
104- See [VENDOR.md](VENDOR.md) for available libraries
105
106**Reference implementations:**
107- Minimal: `apps/home/workspace.html` (simple content)
108- Styled: `apps/dev/workspace.html` (custom CSS, forms, interactive JS)
109- Data-driven: `apps/todos/workspace.html` (facet sections, dynamic rendering)
110
111---
112
113## Optional Files
114
115### 2. `routes.py` - Flask Blueprint
116
117Define custom routes for your app (API endpoints, form handlers, navigation routes).
118
119**Key Points:**
120- **Not needed for simple apps** - the shared handler at `/app/{app_name}` serves your workspace automatically
121- Only create `routes.py` if you need custom routes beyond the index page
122- Blueprint variable must be named `{app_name}_bp`
123- Blueprint name must be `"app:{app_name}"`
124- URL prefix convention: `/app/{app_name}`
125- Access journal root via `state.journal_root` (always available)
126- Import utilities from `convey.utils` (see [Flask Utilities](#flask-utilities))
127
128**Reference implementations:**
129- API endpoints: `apps/search/routes.py` (search APIs, no index route)
130- Form handlers: `apps/todos/routes.py` (POST handlers, validation, flash messages)
131- Navigation: `apps/calendar/routes.py` (date-based routes with custom context)
132- Redirects: `apps/todos/routes.py` index route (redirects `/` to today's date)
133
134
135
136### 3. `app.json` - Metadata
137
138Override default icon, label, and other app settings.
139
140**Authoritative source:** See the `App` dataclass in `apps/__init__.py` for all supported fields, types, and defaults.
141
142**Common fields:**
143- `icon` - Emoji icon for menu bar (default: "📦")
144- `label` - Display label in menu (default: title-cased app name)
145- `facets` - Enable facet integration (default: true)
146- `date_nav` - Show date navigation bar (default: false)
147- `allow_future_dates` - Allow clicking future dates in month picker (default: false)
148
149**When to disable facets:** Set `"facets": false` for apps that don't use facet-based organization (e.g., system settings, dev tools).
150
151**Examples:** Browse `apps/*/app.json` for reference configurations.
152
153### 4. `app_bar.html` - Bottom Bar Controls
154
155Fixed bottom bar for forms, buttons, date pickers, search boxes.
156
157**Key Points:**
158- App bar is fixed to bottom when present
159- Page body gets `has-app-bar` class (adjusts content margin)
160- Only rendered when app provides this template
161- Great for persistent input controls across views
162
163**Date Navigation:**
164
165Enable via `"date_nav": true` in `app.json` (not via includes). This renders a `← Date →` control with month picker. Requires `/app/{app_name}/api/stats/{month}` endpoint returning `{YYYYMMDD: count}` or `{YYYYMMDD: {facet: count}}`.
166
167Keyboard shortcuts: `←`/`→` for day navigation, `t` for today.
168
169### 5. `background.html` - Background Service
170
171JavaScript service that runs globally, even when app is not active.
172
173**AppServices API:**
174
175**Core Methods:**
176- `AppServices.register(appName, service)` - Register background service
177
178**Badge Methods:**
179
180App icon badges (menu bar):
181- `AppServices.badges.app.set(appName, count)` - Set app icon badge count
182- `AppServices.badges.app.clear(appName)` - Remove app icon badge
183- `AppServices.badges.app.get(appName)` - Get current badge count
184
185Facet pill badges (facet bar):
186- `AppServices.badges.facet.set(facetName, count)` - Set facet badge count
187- `AppServices.badges.facet.clear(facetName)` - Remove facet badge
188- `AppServices.badges.facet.get(facetName)` - Get current badge count
189
190Both badge types appear as red notification counts.
191
192**Notification Methods:**
193- `AppServices.notifications.show(options)` - Show persistent notification card
194- `AppServices.notifications.dismiss(id)` - Dismiss specific notification
195- `AppServices.notifications.dismissApp(appName)` - Dismiss all for app
196- `AppServices.notifications.dismissAll()` - Dismiss all notifications
197- `AppServices.notifications.count()` - Get active notification count
198- `AppServices.notifications.update(id, options)` - Update existing notification
199
200**Notification Options:**
201```javascript
202{
203 app: 'my_app', // App name (required)
204 icon: '📬', // Emoji icon (optional)
205 title: 'New Message', // Title (required)
206 message: 'You have...', // Message body (optional)
207 action: '/app/todos', // Click action URL (optional)
208 facet: 'work', // Auto-select facet on click (optional)
209 badge: 5, // Badge count (optional)
210 dismissible: true, // Show X button (default: true)
211 autoDismiss: 10000 // Auto-dismiss ms (optional)
212}
213```
214
215**Submenu Methods:**
216- `AppServices.submenus.set(appName, items)` - Set all submenu items
217- `AppServices.submenus.upsert(appName, item)` - Add or update single item
218- `AppServices.submenus.remove(appName, itemId)` - Remove item by id
219- `AppServices.submenus.clear(appName)` - Clear all items
220
221Submenus appear as hover pop-outs on menu bar icons. Items support `id`, `label`, `icon`, `href`, `facet`, `badge`, and `order` properties.
222
223**See implementation:** `convey/static/app.js` - Submenu rendering and positioning
224
225**WebSocket Events (`window.appEvents`):**
226- `listen(tract, callback)` - Listen to specific tract ('cortex', 'indexer', 'observe', etc.)
227- `listen('*', callback)` - Listen to all events
228- Messages have structure: `{tract: 'cortex', event: 'agent_complete', ...data}`
229- See [CALLOSUM.md](CALLOSUM.md) for event protocol details
230
231**Reference implementations:**
232- `apps/todos/background.html` - App icon badge with API fetch
233- `apps/dev/background.html` - Submenu quick-links with dynamic badges
234
235**Implementation source:** `convey/static/app.js` - AppServices framework, `convey/static/websocket.js` - WebSocket API
236
237---
238
239### 6. `tools.py` - App Tool Functions
240
241Define plain callable tool functions for your app in `tools.py`.
242
243**Key Points:**
244- Only create `tools.py` if your app needs reusable tool functions for agent workflows
245- Keep functions simple: typed inputs, dict-style outputs, clear docstrings
246- Put shared logic in your app/module layer and call it from these functions
247
248**Reference implementations:**
249- `apps/todos/tools.py`
250- `apps/entities/tools.py`
251
252---
253
254### 7. `call.py` - CLI Commands
255
256Define CLI commands for your app that are automatically discovered and available via `sol call <app> <command>`.
257
258**Key Points:**
259- Only create `call.py` if your app needs human-friendly CLI access to its operations
260- Export an `app = typer.Typer()` instance with commands defined via `@app.command()`
261- Automatically discovered and mounted at startup
262- Errors in one app's CLI don't prevent other apps from loading
263- CLI commands call the same data layer as `tools.py` but print formatted console output
264
265**Required export:**
266```python
267import typer
268
269app = typer.Typer(help="Description of your app commands.")
270```
271
272**Command pattern:** Define commands using Typer's `@app.command()` decorator with `typer.Argument` for positional args and `typer.Option` for flags. Call the underlying data layer directly (not tool helper wrappers) and print output via `typer.echo()`.
273
274**CLI vs tool functions:** CLI commands parallel tool functions but are optimized for interactive terminal use. Key differences:
275- Tool functions may accept a `Context` parameter for caller metadata; CLI has no context object
276- Print formatted text instead of returning dicts
277- Use `typer.Exit(1)` for errors instead of returning error dicts
278
279**Discovery behavior:** The `sol call` dispatcher scans `apps/*/call.py` at startup, imports modules, and mounts any `app` variable that is a `typer.Typer` instance as a sub-command. Private apps (directories starting with `_`) are skipped.
280
281**Reference implementations:**
282- Discovery logic: `think/call.py` - `_discover_app_calls()` function
283- App CLI example: `apps/todos/call.py` - Todo list command
284
285---
286
287### 8. `talent/` - App Generators
288
289Define custom generator prompts that integrate with solstone's output generation system.
290
291**Key Points:**
292- Create `talent/` directory with `.md` files containing JSON frontmatter
293- App generators are automatically discovered alongside system generators
294- Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`)
295- Outputs go to `JOURNAL/YYYYMMDD/agents/_<app>_<agent>.md` (or `.json` if `output: "json"`)
296
297**Metadata format:** Same schema as system generators in `talent/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `priority` (required for scheduled prompts), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"`. The `priority` field is required for all scheduled prompts - prompts without explicit priority will fail validation. Set `output: "json"` for structured JSON output instead of markdown. Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted).
298
299**Priority bands:** Prompts run in priority order (lowest first). Recommended bands:
300- 10-30: Generators (content-producing prompts)
301- 40-60: Analysis agents
302- 90+: Late-stage agents
303- 99: Fun/optional prompts
304
305**Event extraction via hooks:** To extract structured events from generator output, use the `hook` field:
306
307- `"hook": {"post": "occurrence"}` - Extracts past events to `facets/{facet}/events/{day}.jsonl`
308- `"hook": {"post": "anticipation"}` - Extracts future scheduled events
309
310The `occurrences` field (optional string) provides agent-specific extraction guidance when using the occurrence hook. Example:
311
312```json
313{
314 "title": "Meeting Summary",
315 "schedule": "daily",
316 "hook": {"post": "occurrence"},
317 "occurrences": "Each meeting should generate an occurrence with start and end times, participants, and summary."
318}
319```
320
321**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/agents/*.md` - these are automatically indexed.
322
323**Template variables:** Generator prompts can use template variables like `$name`, `$preferred`, `$daily_preamble`, and context variables like `$day` and `$day_YYYYMMDD`. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation.
324
325**Custom hooks:** Both generators and tool-using agents support custom `.py` hooks for transforming inputs and outputs programmatically. Hooks support both pre-processing (before LLM call) and post-processing (after LLM call):
326
327**Hook configuration:**
328- Use `"hook": {"pre": "my_hook"}` for pre-processing hooks
329- Use `"hook": {"post": "my_hook"}` for post-processing hooks
330- Use both together: `"hook": {"pre": "prep", "post": "process"}`
331- Use `"hook": {"flush": true}` to opt into segment flush (see below)
332- Resolution: `"name"` → `talent/{name}.py`, `"app:name"` → `apps/{app}/talent/{name}.py`, or explicit path
333
334**Pre-hooks** (`pre_process`): Modify inputs before the LLM call
335- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path`
336- Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`)
337- Return `None` for no changes
338
339**Post-hooks** (`post_process`): Transform output after the LLM call
340- `result` is the LLM output (markdown or JSON string)
341- `context` is the full config dict with: `name`, `agent_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path`
342- Return modified string, or `None` to use original result
343
344**Flush hooks:** Segment agents can declare `"hook": {"flush": true}` to participate in segment flush. When no new segments arrive for an extended period, the supervisor triggers `sol dream --flush --segment <last>`, which runs only flush-enabled agents with `context["flush"] = True` and `context["refresh"] = True`. This lets agents close out dangling state (e.g., end active activities that would otherwise wait indefinitely for the next segment). The timeout is managed by the supervisor — agents should trust the flush signal without their own timeout logic.
345
346Hook errors are logged but don't crash the pipeline (graceful degradation).
347
348```python
349# talent/my_hook.py
350def pre_process(context: dict) -> dict | None:
351 # Modify inputs before LLM call
352 return {"prompt": context["prompt"] + "\n\nBe concise."}
353
354def post_process(result: str, context: dict) -> str | None:
355 # Transform output after LLM call
356 return result + "\n\n## Generated by hook"
357```
358
359**Reference implementations:**
360- System generator templates: `talent/*.md` (files with `schedule` field but no `tools` field)
361- Extraction hooks: `talent/occurrence.py`, `talent/anticipation.py`
362- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=False)`, `get_output_name()`
363- Hook loading: `think/talent.py` - `load_pre_hook()`, `load_post_hook()`
364
365---
366
367### 9. `talent/` - App Agents and Generators
368
369Define custom agents and generator templates that integrate with solstone's Cortex agent system.
370
371**Key Points:**
372- Create `talent/` directory with `.md` files containing JSON frontmatter
373- Both agents and generators live in the same directory - distinguished by frontmatter fields
374- Agents have a `tools` field, generators have `schedule` but no `tools`
375- App agents/generators are automatically discovered alongside system ones
376- Keys are namespaced as `{app}:{name}` (e.g., `my_app:helper`)
377- Agents inherit all system agent capabilities (tools, scheduling, multi-facet)
378
379**Metadata format:** Same schema as system agents in `talent/*.md` - JSON frontmatter includes `title`, `provider`, `model`, `tools`, `schedule`, `priority`, `multi_facet`, `max_output_tokens`, and `thinking_budget` fields. The `priority` field is **required** for all scheduled prompts - prompts without explicit priority will fail validation. See the priority bands documentation in [THINK.md](THINK.md#unified-priority-execution). Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted; OpenAI uses fixed reasoning and ignores this field). See [CORTEX.md](CORTEX.md) for agent configuration details.
380
381**Template variables:** Agent prompts can use template variables like `$name`, `$preferred`, and pronoun variables. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation.
382
383**Reference implementations:**
384- System agent examples: `talent/*.md` (files with `tools` field)
385- Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=True)`, `get_agent()`
386
387#### Prompt Context Configuration
388
389Both generators and agents support an optional `load` key for configuring source data dependencies:
390
391```json
392{
393 "load": {"transcripts": true, "percepts": false, "agents": {"screen": true}}
394}
395```
396
397- `load` controls which source types are clustered before generator execution. Values can be:
398 - `false` - don't load this source type
399 - `true` - load if available
400 - `"required"` - load, and skip generation if no content found (useful for generators that only make sense with specific input types, e.g., `"audio": "required"` for speaker detection)
401 - For `agents` only: a dict for selective filtering, e.g., `{"entities": true, "meetings": "required", "flow": false}`. Keys are agent names (system) or `"app:agent"` (app-namespaced). An empty dict `{}` means no agents.
402
403Context is provided inline in the `.md` body via template variables:
404
405- `$sol_identity` - core identity from `sol/identity.md`
406- `$facets` - focused facet context or all available facets
407- `$activity_context` - activity metadata, segment state, and analysis focus sections
408
409**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_agent_filter()`
410
411---
412
413### 10. `talent/` - Agent Skills
414
415Define [Agent Skills](https://agentskills.io/specification) as subdirectories within `talent/`. Skills package procedural knowledge, workflows, and resources that AI coding agents (Claude Code, GitHub Copilot, Gemini CLI, etc.) can discover and use on demand.
416
417**Key Points:**
418- Create a subdirectory in `talent/` with a `SKILL.md` file (YAML frontmatter + markdown body)
419- The directory name must match the `name` field in the YAML frontmatter
420- Skill names must be unique across system `talent/` and all `apps/*/talent/` directories
421- `make skills` discovers all skills and symlinks them into `.agents/skills/` and `.claude/skills/`
422- Skills are standalone — they don't interact with the talent agent/generator system
423- The talent loader ignores subdirectories, so skills won't interfere with agent discovery
424
425**Directory structure:**
426```
427talent/my-skill/
428├── SKILL.md # Required: YAML frontmatter + instructions
429├── scripts/ # Optional: Executable code (Python, Bash, etc.)
430├── references/ # Optional: Additional documentation loaded on demand
431└── assets/ # Optional: Static resources (templates, data files)
432```
433
434**SKILL.md format:**
435```yaml
436---
437name: my-skill
438description: Short description of what this skill does and when to use it.
439---
440
441# Instructions
442
443Step-by-step procedures, examples, and domain knowledge for the agent.
444```
445
446**Required frontmatter fields:**
447- `name` — Max 64 chars, lowercase letters + numbers + hyphens, must match directory name
448- `description` — Max 1024 chars, describes what the skill does *and when to use it*
449
450**Optional frontmatter fields:**
451- `license` — License name (e.g., `Apache-2.0`)
452- `compatibility` — Max 500 chars, environment requirements
453- `metadata` — Arbitrary key-value string map
454- `allowed-tools` — Space-delimited list of pre-approved tools (experimental)
455
456**App skills** work the same way — place a skill directory inside `apps/my_app/talent/`:
457```
458apps/my_app/talent/my-skill/
459├── SKILL.md
460└── references/
461```
462
463**Running `make skills`:** Discovers all `SKILL.md` files under `talent/*/` and `apps/*/talent/*/`, then creates symlinks so that all supported coding agents see the same skills. Errors if two skills share the same directory name.
464
465---
466
467### 11. `maint/` - Maintenance Tasks
468
469Define one-time maintenance scripts that run automatically on Convey startup.
470
471**Key Points:**
472- Create `maint/` directory with standalone Python scripts (each with a `main()` function)
473- Scripts are discovered and run in sorted order by filename (use `000_`, `001_` prefixes for ordering)
474- Completed tasks tracked in `<journal>/maint/{app}/{task}.jsonl` - runs once per journal
475- Exit code 0 = success, non-zero = failure (failed tasks can be re-run with `--force`)
476- Use `setup_cli()` for consistent argument parsing and logging
477
478**CLI:** `sol maint` (run pending), `sol maint --list` (show status), `sol maint --force` (re-run all)
479
480**Reference implementations:**
481- Example task: `apps/dev/maint/000_example.py` - recommended patterns
482- Discovery logic: `convey/maint.py` - `discover_tasks()`, `run_task()`
483
484---
485
486### 12. `tests/` - App Tests
487
488Apps can include their own tests that are discovered and run separately from core tests.
489
490**Key Points:**
491- Create `tests/` directory with `conftest.py` and `test_*.py` files
492- App fixtures should be self-contained (only use pytest builtins like `tmp_path`, `monkeypatch`)
493- Tests run via `make test-apps` (all apps) or `make test-app APP=my_app`
494- Integration tests can use `@pytest.mark.integration` but live in the same flat structure
495
496**Directory structure:**
497```
498apps/my_app/tests/
499├── __init__.py
500├── conftest.py # Self-contained fixtures
501└── test_*.py # Test files
502```
503
504**Reference implementations:**
505- Fixture patterns: `apps/todos/tests/conftest.py`
506- Tool testing: `apps/todos/tests/test_tools.py`
507
508---
509
510### 13. `events.py` - Server-Side Event Handlers
511
512Define server-side handlers that react to Callosum events. Handlers run in Convey's thread pool, enabling reactive backend logic without creating new services.
513
514**Key Points:**
515- Create `events.py` with functions decorated with `@on_event(tract, event)`
516- Handlers receive an `EventContext` with `msg`, `app`, `tract`, `event` fields
517- Discovered at Convey startup; events processed serially with 30s timeout per handler
518- Errors are logged but don't affect other handlers or the web server
519- Wildcards supported: `@on_event("*", "*")` matches all events
520
521**Available imports** (same as route handlers):
522- `from convey import state` - Access `state.journal_root`
523- `from convey import emit` - Emit events back to Callosum
524- `from apps.utils import get_app_storage_path, log_app_action` - App storage
525- `from convey.utils import load_json, save_json, spawn_agent` - Utilities
526
527**Not available** (no Flask request context):
528- `request`, `session`, `current_app`
529- `error_response()`, `success_response()`, `parse_pagination_params()`
530
531**Reference implementations:**
532- Framework: `apps/events.py` - `EventContext` dataclass, decorator, discovery
533- Example: `apps/dev/events.py` - Debug handler showing usage pattern
534
535---
536
537## Flask Utilities
538
539Available in `convey/utils.py`:
540
541### Route Helpers
542- `error_response(message, code=400)` - Standard JSON error response
543- `success_response(data=None, code=200)` - Standard JSON success response
544- `parse_pagination_params(default_limit, max_limit, min_limit)` - Extract and validate limit/offset from request.args
545
546### Date Formatting
547- `format_date(date_str)` - Format YYYYMMDD as "Wednesday January 14th"
548
549### Agent Spawning
550- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns agent_id
551
552### JSON Utilities
553- `load_json(path)` - Load JSON file with error handling (returns None on error)
554- `save_json(path, data, indent, add_newline)` - Save JSON with formatting (returns bool)
555
556**See source:** `convey/utils.py` for full signatures and documentation
557
558### App Storage
559
560Apps can persist journal-specific configuration and data in `<journal>/apps/<app_name>/`:
561
562```python
563from apps.utils import get_app_storage_path, load_app_config, save_app_config
564```
565
566- `get_app_storage_path(app_name, *sub_dirs, ensure_exists)` - Get Path to app storage directory
567- `load_app_config(app_name, default)` - Load app config from `config.json`
568- `save_app_config(app_name, config)` - Save app config to `config.json`
569
570**See source:** `apps/utils.py` for implementation details
571
572### Action Logging
573
574Apps that modify owner data should log actions for audit trail purposes:
575
576```python
577from apps.utils import log_app_action
578```
579
580- `log_app_action(app, facet, action, params, day=None)` - Log owner-initiated action
581
582**Parameters:**
583- `app` - App name where action originated
584- `facet` - Facet where action occurred, or `None` for journal-level actions
585- `action` - Action type using `{domain}_{verb}` naming (e.g., `entity_add`, `todo_complete`)
586- `params` - Action-specific parameters dict
587- `day` - Optional day in YYYYMMDD format (defaults to today)
588
589**Facet-scoped vs journal-level:**
590- Pass a facet name for facet-specific actions (todos, entities, etc.)
591- Pass `facet=None` for journal-level actions (settings, observers, etc.)
592
593Log after successful mutations, not attempts.
594
595---
596
597## Think Module Integration
598
599Available functions from the `think` module:
600
601### Facets
602`think/facets.py`: `get_facets()` - Returns dict of facet configurations
603
604### Todos
605`apps/todos/todo.py`:
606- `get_todos(day, facet)` - Get todo list for day and facet
607- `TodoChecklist` class - Load and manipulate todo markdown files
608
609### Entities
610`think/entities/`: `load_entities(facet)` - Load entities for a facet
611
612See [JOURNAL.md](JOURNAL.md), [CORTEX.md](CORTEX.md), [CALLOSUM.md](CALLOSUM.md) for subsystem details.
613
614---
615
616## JavaScript APIs
617
618### Global Variables
619
620Defined in `convey/templates/app.html`:
621- `window.facetsData` - Array of facet objects `[{name, title, color, emoji}, ...]`
622- `window.selectedFacet` - Current facet name or null (see Facet Selection below)
623- `window.appFacetCounts` - Badge counts for current app `{"work": 5, "personal": 3}` (set via route's `facet_counts`)
624
625### Facet Selection
626
627Apps can access and control facet selection through a uniform API:
628- `window.selectedFacet` - Current facet name or null (initialized by server, updated on change)
629- `window.selectFacet(name)` - Change selection programmatically
630- `facet.switch` CustomEvent - Dispatched when selection changes
631 - Event detail: `{facet: 'work' or null, facetData: {name, title, color, emoji} or null}`
632
633**Facet Modes:**
634- **all-facet mode**: `window.selectedFacet === null`, show content from all facets
635- **specific-facet mode**: `window.selectedFacet === "work"`, show only that facet's content
636- Selection persisted via cookie, synchronized across facet pills
637
638**UX Tip:** Apps should provide visual indication when in all-facet mode vs showing a specific facet. For example, group items by facet, show facet badges/colors on items, or display a subtle "All facets" label. This helps owners understand the scope of what they're viewing.
639
640**See implementation:** `convey/static/app.js` - Facet switching logic and event dispatch
641
642**Disabled mode:** On apps with `facets.disabled: true`, the facet bar is visible but inert — pills render without interactivity or tab stops. The container is marked `aria-hidden="true"` so screen readers skip it. The bar remains visually present as always-visible chrome.
643
644### WebSocket Events (Client-Side)
645
646`window.appEvents` API defined in `convey/static/websocket.js`:
647- `listen(tract, callback)` - Subscribe to specific tract or '*' for all events
648- Messages structure: `{tract: 'cortex', event: 'agent_complete', ...data}`
649
650**Common tracts:** `cortex`, `indexer`, `observe`, `task`
651
652See [CALLOSUM.md](CALLOSUM.md) for complete event protocol.
653
654### Server-Side Events
655
656Emit Callosum events from route handlers using `convey.emit()`:
657
658```python
659from convey import emit
660
661@my_bp.route("/action", methods=["POST"])
662def handle_action():
663 # ... process request ...
664
665 # Emit event (non-blocking, drops if disconnected)
666 emit("my_app", "action_complete", item_id=123, status="success")
667
668 return jsonify({"status": "ok"})
669```
670
671**Behavior:**
672- Non-blocking: queues message for background thread
673- If Callosum disconnected, message is dropped (with debug logging)
674- Returns `True` if queued, `False` if bridge not started or queue full
675
676**Reference implementations:** `apps/import/routes.py`, `apps/observer/routes.py`
677
678---
679
680## CSS Styling
681
682### Workspace Containers
683
684**Always wrap your workspace content** in one of these standardized containers for consistent spacing and layout:
685
686**For readable content** (forms, lists, messages, text):
687```html
688<div class="workspace-content">
689 <!-- Your app content here -->
690</div>
691```
692
693**For data-heavy content** (tables, grids, calendars):
694```html
695<div class="workspace-content-wide">
696 <!-- Your app content here -->
697</div>
698```
699
700**Key differences:**
701- `.workspace-content` - Centered with 1200px max-width, ideal for readability
702- `.workspace-content-wide` - Full viewport width, ideal for data tables and grids
703- Both include consistent padding and mobile responsiveness
704
705**See:** `convey/static/app.css` for implementation details
706
707**Examples:**
708- Standard: `apps/home/workspace.html`, `apps/todos/workspace.html`, `apps/entities/workspace.html`
709- Wide: `apps/search/workspace.html`, `apps/calendar/_day.html`, `apps/import/workspace.html`
710
711### CSS Variables
712
713Dynamic variables based on selected facet (update automatically on facet change):
714
715```css
716:root {
717 --facet-color: #3b82f6; /* Selected facet color */
718 --facet-bg: #3b82f61a; /* 10% opacity background */
719 --facet-border: #3b82f6; /* Border color */
720}
721```
722
723Use these in your app-specific styles to respond to facet theme.
724
725### App-Specific Styles
726
727**Best practice:** Scope styles with unique class prefix to avoid conflicts.
728
729**Example:** `apps/dev/workspace.html` shows scoped `.dev-*` classes for all custom styles in its `<style>` block.
730
731### Global Styles
732
733Main stylesheet `convey/static/app.css` provides base components. Review for available classes and patterns.
734
735---
736
737## Common Patterns
738
739### Date-Based Navigation
740See `apps/todos/routes.py:todos_day()` - Shows date validation and `format_date()` usage. Day navigation is handled automatically by the date_nav component.
741
742### AJAX Endpoints
743See `apps/todos/routes.py:move_todo()` - Shows JSON parsing, validation, `error_response()`, `success_response()`.
744
745### Form Handling with Flash Messages
746See `apps/todos/routes.py:todos_day()` POST handler - Shows form processing, validation, flash messages, redirects.
747
748### Facet-Aware Queries
749See `apps/todos/routes.py:todos_day()` - Loads data per-facet when selected, or all facets when null.
750
751### Facet Pill Badges
752Pass `facet_counts` dict to `render_template()` to show initial badge counts on facet pills:
753```python
754facet_counts = {"work": 5, "personal": 3}
755return render_template("app.html", facet_counts=facet_counts)
756```
757For client-side updates (e.g., after completing a todo), use `AppServices.badges.facet.set(facetName, count)`.
758
759See `apps/todos/routes.py:todos_day()` - Computes pending counts from already-loaded data.
760
761---
762
763## Debugging Tips
764
765### Check Discovery
766
767```bash
768# Start Convey with debug logging
769FLASK_DEBUG=1 convey
770
771# Look for log lines:
772# "Discovered app: my_app"
773# "Registered blueprint: app:my_app"
774```
775
776### Common Issues
777
778| Issue | Cause | Fix |
779|-------|-------|-----|
780| App not discovered | Missing `workspace.html` | Ensure workspace.html exists |
781| Blueprint not found (with routes.py) | Wrong variable name | Use `{app_name}_bp` exactly |
782| Import error (with routes.py) | Blueprint name mismatch | Use `"app:{app_name}"` exactly |
783| Hyphens in name | Directory uses hyphens | Rename to use underscores |
784| Custom routes don't work | URL prefix mismatch | Check `url_prefix` matches pattern |
785
786### Logging
787
788Use `current_app.logger` from Flask for debugging. See `apps/todos/routes.py` for examples.
789
790---
791
792## Best Practices
793
7941. **Use underscores** in directory names (`my_app`, not `my-app`)
7952. **Wrap workspace content** in `.workspace-content` or `.workspace-content-wide`
7963. **Scope CSS** with unique class names to avoid conflicts
7974. **Validate input** on all POST endpoints (use `error_response`)
7985. **Check facet selection** when loading facet-specific data
7996. **Use state.journal_root** for journal path (always available)
8007. **Pass facet_counts** from routes if app has per-facet counts
8018. **Handle errors gracefully** with flash messages or JSON errors
8029. **Test facet switching** to ensure content updates correctly
80310. **Use background services** for WebSocket event handling
80411. **Follow Flask patterns** for blueprints, url_for, etc.
805
806---
807
808## Example Apps
809
810Browse `apps/*/` directories for reference implementations. Apps range in complexity:
811
812- **Minimal** - Just `workspace.html` (e.g., `apps/home/`, `apps/health/`)
813- **Styled** - Custom CSS, background services (e.g., `apps/dev/`)
814- **Full-featured** - Routes, forms, AJAX, badges, tools (e.g., `apps/todos/`, `apps/entities/`)
815
816---
817
818## Additional Resources
819
820- **`apps/__init__.py`** - App discovery and registry implementation
821- **`convey/apps.py`** - Context processors and vendor library helper
822- **`convey/templates/app.html`** - Main app container template
823- **`convey/static/app.js`** - AppServices framework
824- **`convey/static/websocket.js`** - WebSocket event system
825- [../AGENTS.md](../AGENTS.md) - Project development guidelines and standards
826- [JOURNAL.md](JOURNAL.md) - Journal directory structure and data organization
827- [CORTEX.md](CORTEX.md) - Agent system architecture and spawning agents
828- [CALLOSUM.md](CALLOSUM.md) - Message bus protocol and WebSocket events
829
830For Flask documentation, see [https://flask.palletsprojects.com/](https://flask.palletsprojects.com/)