personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 829 lines 38 kB view raw view rendered
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 `&#123;&#123; vendor_lib('marked') &#125;&#125;` 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/support/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/activities/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- `AppServices.escapeHtml(value)` - DOM-based HTML escaping helper; null/undefined become `''` 178- `AppServices.renderMarkdown(raw)` - `marked` + `DOMPurify` markdown rendering with `{ breaks: true, gfm: true }`; requires both libraries loaded 179 180**Badge Methods:** 181 182App icon badges (menu bar): 183- `AppServices.badges.app.set(appName, count)` - Set app icon badge count 184- `AppServices.badges.app.clear(appName)` - Remove app icon badge 185- `AppServices.badges.app.get(appName)` - Get current badge count 186 187Facet pill badges (facet bar): 188- `AppServices.badges.facet.set(facetName, count)` - Set facet badge count 189- `AppServices.badges.facet.clear(facetName)` - Remove facet badge 190- `AppServices.badges.facet.get(facetName)` - Get current badge count 191 192Both badge types appear as red notification counts. 193 194**Notification Methods:** 195- `AppServices.notifications.show(options)` - Show persistent notification card 196- `AppServices.notifications.dismiss(id)` - Dismiss specific notification 197- `AppServices.notifications.dismissApp(appName)` - Dismiss all for app 198- `AppServices.notifications.dismissAll()` - Dismiss all notifications 199- `AppServices.notifications.count()` - Get active notification count 200- `AppServices.notifications.update(id, options)` - Update existing notification 201 202**Notification Options:** 203```javascript 204{ 205 app: 'my_app', // App name (required) 206 icon: '📬', // Emoji icon (optional) 207 title: 'New Message', // Title (required) 208 message: 'You have...', // Message body (optional) 209 action: '/app/todos', // Click action URL (optional) 210 facet: 'work', // Auto-select facet on click (optional) 211 badge: 5, // Badge count (optional) 212 dismissible: true, // Show X button (default: true) 213 autoDismiss: 10000 // Auto-dismiss ms (optional) 214} 215``` 216 217**WebSocket Events (`window.appEvents`):** 218- `listen(tract, callback)` - Listen to specific tract ('cortex', 'indexer', 'observe', etc.) 219- `listen('*', callback)` - Listen to all events 220- Messages have structure: `{tract: 'cortex', event: 'agent_complete', ...data}` 221- See [CALLOSUM.md](CALLOSUM.md) for event protocol details 222 223**Reference implementations:** 224- `apps/todos/background.html` - App icon badge with API fetch 225 226**Implementation source:** `convey/static/app.js` - AppServices framework, `convey/static/websocket.js` - WebSocket API 227 228--- 229 230### 6. `tools.py` - App Tool Functions 231 232Define plain callable tool functions for your app in `tools.py`. 233 234**Key Points:** 235- Only create `tools.py` if your app needs reusable tool functions for agent workflows 236- Keep functions simple: typed inputs, dict-style outputs, clear docstrings 237- Put shared logic in your app/module layer and call it from these functions 238 239**Reference implementations:** 240- `apps/todos/tools.py` 241- `apps/entities/tools.py` 242 243--- 244 245### 7. `call.py` - CLI Commands 246 247Define CLI commands for your app that are automatically discovered and available via `sol call <app> <command>`. 248 249**Key Points:** 250- Only create `call.py` if your app needs human-friendly CLI access to its operations 251- Export an `app = typer.Typer()` instance with commands defined via `@app.command()` 252- Automatically discovered and mounted at startup 253- Errors in one app's CLI don't prevent other apps from loading 254- CLI commands call the same data layer as `tools.py` but print formatted console output 255 256**Required export:** 257```python 258import typer 259 260app = typer.Typer(help="Description of your app commands.") 261``` 262 263**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()`. 264 265**CLI vs tool functions:** CLI commands parallel tool functions but are optimized for interactive terminal use. Key differences: 266- Tool functions may accept a `Context` parameter for caller metadata; CLI has no context object 267- Print formatted text instead of returning dicts 268- Use `typer.Exit(1)` for errors instead of returning error dicts 269 270**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. 271 272**Reference implementations:** 273- Discovery logic: `think/call.py` - `_discover_app_calls()` function 274- App CLI example: `apps/todos/call.py` - Todo list command 275 276**Skills app reference:** `apps/skills/call.py` is the current owner-wide pattern for a data-backed app CLI. It exposes `sol call skills list|show|observe|seed|promote|refresh|mark-dormant|retire|edit-request|rename` and routes all writes through `think/skills.py`, which owns `journal/skills/patterns.jsonl`, `journal/skills/edit_requests.jsonl`, and `journal/skills/{slug}.md`. The shipped daily talents for this app live in `apps/skills/talent/skill_observer.md` (daily cogitate, priority 41) and `apps/skills/talent/skill_editor.md` + `skill_editor.py` (daily generate, priority 60). The observer marks patterns for creation/refresh, and the editor consumes those flags or pending `edit-request` rows to write/update exactly one owner-wide profile per run. 277 278--- 279 280### 8. `talent/` - App Generators 281 282Define custom generator prompts that integrate with solstone's output generation system. 283 284**Key Points:** 285- Create `talent/` directory with `.md` files containing JSON frontmatter 286- App generators are automatically discovered alongside system generators 287- Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`) 288- Outputs go to `JOURNAL/YYYYMMDD/talents/_<app>_<agent>.md` (or `.json` if `output: "json"`) 289 290**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). Generators reject a `cwd` field entirely; working-directory control is only available for `type: "cogitate"` prompts. 291 292**Priority bands:** Prompts run in priority order (lowest first). Recommended bands: 293- 10-30: Generators (content-producing prompts) 294- 40-60: Analysis agents 295- 90+: Late-stage agents 296- 99: Fun/optional prompts 297 298**Schedule extraction via hooks:** The live built-in extraction hook is `schedule`: 299 300- `"hook": {"post": "schedule"}` - Writes future scheduled items to `facets/{facet}/activities/{target_day}.jsonl` as anticipated activity records 301 302Example: 303 304```json 305{ 306 "title": "Schedule Extractor", 307 "schedule": "daily", 308 "hook": {"post": "schedule"} 309} 310``` 311 312**App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/talents/*.md` - these are automatically indexed. 313 314**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. 315 316**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): 317 318**Hook configuration:** 319- Use `"hook": {"pre": "my_hook"}` for pre-processing hooks 320- Use `"hook": {"post": "my_hook"}` for post-processing hooks 321- Use both together: `"hook": {"pre": "prep", "post": "process"}` 322- Use `"hook": {"flush": true}` to opt into segment flush (see below) 323- Resolution: `"name"``talent/{name}.py`, `"app:name"``apps/{app}/talent/{name}.py`, or explicit path 324 325**Pre-hooks** (`pre_process`): Modify inputs before the LLM call 326- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `system_instruction` (if set), `user_instruction`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` 327- Return a dict of modified fields to merge back (e.g., `{"prompt": "modified"}`) 328- Return `None` for no changes 329 330**Post-hooks** (`post_process`): Transform output after the LLM call 331- `result` is the LLM output (markdown or JSON string) 332- `context` is the full config dict with: `name`, `use_id`, `provider`, `model`, `prompt`, `output`, `meta`, and for generators: `day`, `segment`, `span`, `span_mode`, `transcript`, `output_path` 333- Return modified string, or `None` to use original result 334 335**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 think --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. 336 337Hook errors are logged but don't crash the pipeline (graceful degradation). 338 339```python 340# talent/my_hook.py 341def pre_process(context: dict) -> dict | None: 342 # Modify inputs before LLM call 343 return {"prompt": context["prompt"] + "\n\nBe concise."} 344 345def post_process(result: str, context: dict) -> str | None: 346 # Transform output after LLM call 347 return result + "\n\n## Generated by hook" 348``` 349 350**Hook idempotency:** Post-hooks that write to shared journal state must be safe to run more than once on the same inputs. `sol think --refresh` bypasses the "output already exists" early-return in `think/talents.py` and re-executes the talent, which re-fires `post_process` against a fresh LLM result — so any side-effect the hook performs (writing events, appending to a log, updating an index file) will happen again. Pick one of these two patterns: 351 352- **Natural-key dedup.** Read the existing output, compute a natural key per row (e.g., `(facet, event_day, title, start, end)` for facet events), skip rows already present, and append only the new ones. Use this when the output is append-only history and you want to preserve prior writes from other agents. 353- **Atomic replace.** Recompute the full output, write it to a temp file, and rename into place. `atomic_write()` in `think/entities/core.py` is the established helper for text outputs; for JSONL, write the full set of lines to a tempfile and `os.replace()`. Use this when the hook owns the file end-to-end. 354 355(Retired 2026-04-18 Sprint 4.) An earlier `write_events_jsonl` hook in `think/hooks.py` opened facet-event logs in `"a"` mode with no dedup and doubled row counts on every `sol think --refresh` — see the 2026-04-17 layer-violations audit (V6) tracked in sol pbc's internal engineering notes for the full write-up. 356 357See `docs/coding-standards.md` L8/L9 for the broader principles. 358 359**Reference implementations:** 360- System generator templates: `talent/*.md` (files with `schedule` field but no `tools` field) 361- Schedule hook: `talent/schedule.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). Cogitate agents may also declare `cwd: "journal"` or `cwd: "repo"`; when omitted they default to `journal`, and repo-oriented prompts like `coder` should opt into `repo`. 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_talent()` 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, "talents": {"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- `$facets` - focused facet context or all available facets 406- `$activity_context` - activity metadata, segment state, and analysis focus sections 407 408**Authoritative source:** `think/talent.py` - `_DEFAULT_LOAD`, `source_is_enabled()`, `source_is_required()`, `get_talent_filter()` 409 410--- 411 412### 10. `talent/` - Agent Skills 413 414Define [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. 415 416**Key Points:** 417- Create a subdirectory in `talent/` with a `SKILL.md` file (YAML frontmatter + markdown body) 418- The directory name must match the `name` field in the YAML frontmatter 419- Skill names must be unique across system `talent/` and all `apps/*/talent/` directories 420- `make skills` discovers all skills and symlinks them into `journal/.agents/skills/` and `journal/.claude/skills/` 421- Skills are standalone — they don't interact with the talent agent/generator system 422- The talent loader ignores subdirectories, so skills won't interfere with agent discovery 423 424**Directory structure:** 425``` 426talent/my-skill/ 427├── SKILL.md # Required: YAML frontmatter + instructions 428├── scripts/ # Optional: Executable code (Python, Bash, etc.) 429├── references/ # Optional: Additional documentation loaded on demand 430└── assets/ # Optional: Static resources (templates, data files) 431``` 432 433**SKILL.md format:** 434```yaml 435--- 436name: my-skill 437description: Short description of what this skill does and when to use it. 438--- 439 440# Instructions 441 442Step-by-step procedures, examples, and domain knowledge for the agent. 443``` 444 445**Required frontmatter fields:** 446- `name` — Max 64 chars, lowercase letters + numbers + hyphens, must match directory name 447- `description` — Max 1024 chars, describes what the skill does *and when to use it* 448 449**Optional frontmatter fields:** 450- `license` — License name (e.g., `Apache-2.0`) 451- `compatibility` — Max 500 chars, environment requirements 452- `metadata` — Arbitrary key-value string map 453- `allowed-tools` — Space-delimited list of pre-approved tools (experimental) 454 455**App skills** work the same way — place a skill directory inside `apps/my_app/talent/`: 456``` 457apps/my_app/talent/my-skill/ 458├── SKILL.md 459└── references/ 460``` 461 462**Running `make skills`:** Discovers all `SKILL.md` files under `talent/*/` and `apps/*/talent/*/`, then creates symlinks in `journal/.agents/skills/` and `journal/.claude/skills/` so that all supported coding agents see the same skills. Errors if two skills share the same directory name. 463 464--- 465 466### 11. `maint/` - Maintenance Tasks 467 468Define one-time maintenance scripts that run automatically when supervisor starts. 469 470**Key Points:** 471- Create `maint/` directory with standalone Python scripts (each with a `main()` function) 472- Scripts are discovered and run in sorted order by filename (use `000_`, `001_` prefixes for ordering) 473- Completed tasks tracked in `<journal>/maint/{app}/{task}.jsonl` - runs once per journal 474- Exit code 0 = success, non-zero = failure (failed tasks can be re-run with `--force`) 475- Use `setup_cli()` for consistent argument parsing and logging 476 477**CLI:** `sol maint` (run pending), `sol maint --list` (show status), `sol maint --force` (re-run all) 478 479**Reference implementations:** 480- Example task: `apps/entities/maint/001_migrate_to_journal_entities.py` - real migration task demonstrating maint patterns 481- Discovery logic: `think/maint.py` - `discover_tasks()`, `run_task()` 482 483--- 484 485### 12. `tests/` - App Tests 486 487Apps can include their own tests that are discovered and run separately from core tests. 488 489**Key Points:** 490- Create `tests/` directory with `conftest.py` and `test_*.py` files 491- App fixtures should be self-contained (only use pytest builtins like `tmp_path`, `monkeypatch`) 492- Tests run via `make test-apps` (all apps) or `make test-app APP=my_app` 493- Integration tests can use `@pytest.mark.integration` but live in the same flat structure 494 495**Directory structure:** 496``` 497apps/my_app/tests/ 498├── __init__.py 499├── conftest.py # Self-contained fixtures 500└── test_*.py # Test files 501``` 502 503**Reference implementations:** 504- Fixture patterns: `apps/todos/tests/conftest.py` 505- Tool testing: `apps/todos/tests/test_tools.py` 506 507--- 508 509### 13. `events.py` - Server-Side Event Handlers 510 511Define server-side handlers that react to Callosum events. Handlers run in Convey's thread pool, enabling reactive backend logic without creating new services. 512 513**Key Points:** 514- Create `events.py` with functions decorated with `@on_event(tract, event)` 515- Handlers receive an `EventContext` with `msg`, `app`, `tract`, `event` fields 516- Discovered at Convey startup; events processed serially with 30s timeout per handler 517- Errors are logged but don't affect other handlers or the web server 518- Wildcards supported: `@on_event("*", "*")` matches all events 519 520**Available imports** (same as route handlers): 521- `from convey import state` - Access `state.journal_root` 522- `from convey import emit` - Emit events back to Callosum 523- `from apps.utils import get_app_storage_path, log_app_action` - App storage 524- `from convey.utils import load_json, save_json, spawn_agent` - Utilities 525 526**Not available** (no Flask request context): 527- `request`, `session`, `current_app` 528- `error_response()`, `success_response()`, `parse_pagination_params()` 529 530**Reference implementations:** 531- Framework: `apps/events.py` - `EventContext` dataclass, decorator, discovery 532- Example: `apps/entities/events.py` - Entity activity tracking via event handlers 533 534--- 535 536## Flask Utilities 537 538Available in `convey/utils.py`: 539 540### Route Helpers 541- `error_response(message, code=400)` - Standard JSON error response 542- `success_response(data=None, code=200)` - Standard JSON success response 543- `parse_pagination_params(default_limit, max_limit, min_limit)` - Extract and validate limit/offset from request.args 544 545### Date Formatting 546- `format_date(date_str)` - Format YYYYMMDD as "Wednesday January 14th" 547 548### Agent Spawning 549- `spawn_agent(prompt, name, provider, config)` - Spawn Cortex agent, returns use_id 550 551### JSON Utilities 552- `load_json(path)` - Load JSON file with error handling (returns None on error) 553- `save_json(path, data, indent, add_newline)` - Save JSON with formatting (returns bool) 554 555**See source:** `convey/utils.py` for full signatures and documentation 556 557### App Storage 558 559Apps can persist journal-specific configuration and data in `<journal>/apps/<app_name>/`: 560 561```python 562from apps.utils import get_app_storage_path, load_app_config, save_app_config 563``` 564 565- `get_app_storage_path(app_name, *sub_dirs, ensure_exists)` - Get Path to app storage directory 566- `load_app_config(app_name, default)` - Load app config from `config.json` 567- `save_app_config(app_name, config)` - Save app config to `config.json` 568 569**See source:** `apps/utils.py` for implementation details 570 571### Action Logging 572 573Apps that modify owner data should log actions for audit trail purposes: 574 575```python 576from apps.utils import log_app_action 577``` 578 579- `log_app_action(app, facet, action, params, day=None)` - Log owner-initiated action 580 581**Parameters:** 582- `app` - App name where action originated 583- `facet` - Facet where action occurred, or `None` for journal-level actions 584- `action` - Action type using `{domain}_{verb}` naming (e.g., `entity_add`, `todo_complete`) 585- `params` - Action-specific parameters dict 586- `day` - Optional day in YYYYMMDD format (defaults to today) 587 588**Facet-scoped vs journal-level:** 589- Pass a facet name for facet-specific actions (todos, entities, etc.) 590- Pass `facet=None` for journal-level actions (settings, observers, etc.) 591 592Log after successful mutations, not attempts. 593 594--- 595 596## Think Module Integration 597 598Available functions from the `think` module: 599 600### Facets 601`think/facets.py`: `get_facets()` - Returns dict of facet configurations 602 603### Todos 604`apps/todos/todo.py`: 605- `get_todos(day, facet)` - Get todo list for day and facet 606- `TodoChecklist` class - Load and manipulate todo markdown files 607 608### Entities 609`think/entities/`: `load_entities(facet)` - Load entities for a facet 610 611See [talent/journal/SKILL.md](../talent/journal/SKILL.md), [CORTEX.md](CORTEX.md), [CALLOSUM.md](CALLOSUM.md) for subsystem details. 612 613--- 614 615## JavaScript APIs 616 617### Global Variables 618 619Defined in `convey/templates/app.html`: 620- `window.facetsData` - Array of facet objects `[{name, title, color, emoji}, ...]` 621- `window.selectedFacet` - Current facet name or null (see Facet Selection below) 622- `window.appFacetCounts` - Badge counts for current app `{"work": 5, "personal": 3}` (set via route's `facet_counts`) 623 624### Facet Selection 625 626Apps can access and control facet selection through a uniform API: 627- `window.selectedFacet` - Current facet name or null (initialized by server, updated on change) 628- `window.selectFacet(name)` - Change selection programmatically 629- `facet.switch` CustomEvent - Dispatched when selection changes 630 - Event detail: `{facet: 'work' or null, facetData: {name, title, color, emoji} or null}` 631 632**Facet Modes:** 633- **all-facet mode**: `window.selectedFacet === null`, show content from all facets 634- **specific-facet mode**: `window.selectedFacet === "work"`, show only that facet's content 635- Selection persisted via cookie, synchronized across facet pills 636 637**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. 638 639**See implementation:** `convey/static/app.js` - Facet switching logic and event dispatch 640 641**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. 642 643### WebSocket Events (Client-Side) 644 645`window.appEvents` API defined in `convey/static/websocket.js`: 646- `listen(tract, callback)` - Subscribe to specific tract or '*' for all events 647- Messages structure: `{tract: 'cortex', event: 'agent_complete', ...data}` 648 649**Common tracts:** `cortex`, `indexer`, `observe`, `task` 650 651See [CALLOSUM.md](CALLOSUM.md) for complete event protocol. 652 653### Server-Side Events 654 655Emit Callosum events from route handlers using `convey.emit()`: 656 657```python 658from convey import emit 659 660@my_bp.route("/action", methods=["POST"]) 661def handle_action(): 662 # ... process request ... 663 664 # Emit event (non-blocking, drops if disconnected) 665 emit("my_app", "action_complete", item_id=123, status="success") 666 667 return jsonify({"status": "ok"}) 668``` 669 670**Behavior:** 671- Non-blocking: queues message for background thread 672- If Callosum disconnected, message is dropped (with debug logging) 673- Returns `True` if queued, `False` if bridge not started or queue full 674 675**Reference implementations:** `apps/import/routes.py`, `apps/observer/routes.py` 676 677--- 678 679## CSS Styling 680 681### Workspace Containers 682 683**Always wrap your workspace content** in one of these standardized containers for consistent spacing and layout: 684 685**For readable content** (forms, lists, messages, text): 686```html 687<div class="workspace-content"> 688 <!-- Your app content here --> 689</div> 690``` 691 692**For data-heavy content** (tables, grids, calendars): 693```html 694<div class="workspace-content-wide"> 695 <!-- Your app content here --> 696</div> 697``` 698 699**Key differences:** 700- `.workspace-content` - Centered with 1200px max-width, ideal for readability 701- `.workspace-content-wide` - Full viewport width, ideal for data tables and grids 702- Both include consistent padding and mobile responsiveness 703 704**See:** `convey/static/app.css` for implementation details 705 706**Examples:** 707- Standard: `apps/home/workspace.html`, `apps/todos/workspace.html`, `apps/entities/workspace.html` 708- Wide: `apps/search/workspace.html`, `apps/activities/_day.html`, `apps/import/workspace.html` 709 710### CSS Variables 711 712Dynamic variables based on selected facet (update automatically on facet change): 713 714```css 715:root { 716 --facet-color: #3b82f6; /* Selected facet color */ 717 --facet-bg: #3b82f61a; /* 10% opacity background */ 718 --facet-border: #3b82f6; /* Border color */ 719} 720``` 721 722Use these in your app-specific styles to respond to facet theme. 723 724### App-Specific Styles 725 726**Best practice:** Scope styles with unique class prefix to avoid conflicts. 727 728**Example:** `apps/stats/workspace.html` shows scoped `.stats-*` classes for all custom styles in its `<style>` block. 729 730### Global Styles 731 732Main stylesheet `convey/static/app.css` provides base components. Review for available classes and patterns. 733 734--- 735 736## Common Patterns 737 738### Date-Based Navigation 739See `apps/todos/routes.py:todos_day()` - Shows date validation and `format_date()` usage. Day navigation is handled automatically by the date_nav component. 740 741### AJAX Endpoints 742See `apps/todos/routes.py:move_todo()` - Shows JSON parsing, validation, `error_response()`, `success_response()`. 743 744### Form Handling with Flash Messages 745See `apps/todos/routes.py:todos_day()` POST handler - Shows form processing, validation, flash messages, redirects. 746 747### Facet-Aware Queries 748See `apps/todos/routes.py:todos_day()` - Loads data per-facet when selected, or all facets when null. 749 750### Facet Pill Badges 751Pass `facet_counts` dict to `render_template()` to show initial badge counts on facet pills: 752```python 753facet_counts = {"work": 5, "personal": 3} 754return render_template("app.html", facet_counts=facet_counts) 755``` 756For client-side updates (e.g., after completing a todo), use `AppServices.badges.facet.set(facetName, count)`. 757 758See `apps/todos/routes.py:todos_day()` - Computes pending counts from already-loaded data. 759 760--- 761 762## Debugging Tips 763 764### Check Discovery 765 766```bash 767# Start Convey with debug logging 768FLASK_DEBUG=1 convey 769 770# Look for log lines: 771# "Discovered app: my_app" 772# "Registered blueprint: app:my_app" 773``` 774 775### Common Issues 776 777| Issue | Cause | Fix | 778|-------|-------|-----| 779| App not discovered | Missing `workspace.html` | Ensure workspace.html exists | 780| Blueprint not found (with routes.py) | Wrong variable name | Use `{app_name}_bp` exactly | 781| Import error (with routes.py) | Blueprint name mismatch | Use `"app:{app_name}"` exactly | 782| Hyphens in name | Directory uses hyphens | Rename to use underscores | 783| Custom routes don't work | URL prefix mismatch | Check `url_prefix` matches pattern | 784 785### Logging 786 787Use `current_app.logger` from Flask for debugging. See `apps/todos/routes.py` for examples. 788 789--- 790 791## Best Practices 792 7931. **Use underscores** in directory names (`my_app`, not `my-app`) 7942. **Wrap workspace content** in `.workspace-content` or `.workspace-content-wide` 7953. **Scope CSS** with unique class names to avoid conflicts 7964. **Validate input** on all POST endpoints (use `error_response`) 7975. **Check facet selection** when loading facet-specific data 7986. **Use state.journal_root** for journal path (always available) 7997. **Pass facet_counts** from routes if app has per-facet counts 8008. **Handle errors gracefully** with flash messages or JSON errors 8019. **Test facet switching** to ensure content updates correctly 80210. **Use background services** for WebSocket event handling 80311. **Follow Flask patterns** for blueprints, url_for, etc. 804 805--- 806 807## Example Apps 808 809Browse `apps/*/` directories for reference implementations. Apps range in complexity: 810 811- **Minimal** - Just `workspace.html` (e.g., `apps/home/`, `apps/health/`) 812- **Styled** - Custom CSS, background services (e.g., `apps/support/`) 813- **Full-featured** - Routes, forms, AJAX, badges, tools (e.g., `apps/todos/`, `apps/entities/`) 814 815--- 816 817## Additional Resources 818 819- **`apps/__init__.py`** - App discovery and registry implementation 820- **`convey/apps.py`** - Context processors and vendor library helper 821- **`convey/templates/app.html`** - Main app container template 822- **`convey/static/app.js`** - AppServices framework 823- **`convey/static/websocket.js`** - WebSocket event system 824- [../AGENTS.md](../AGENTS.md) - Project development guidelines and standards 825- [storage.md](../talent/journal/references/storage.md) - Journal directory structure and data organization 826- [CORTEX.md](CORTEX.md) - Agent system architecture and spawning agents 827- [CALLOSUM.md](CALLOSUM.md) - Message bus protocol and WebSocket events 828 829For Flask documentation, see [https://flask.palletsprojects.com/](https://flask.palletsprojects.com/)