mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-11 08:12:38 +02:00
feat: tracks — frontmatter directives, sidebar UI, multi-trigger
Recasts the old "track blocks" as "tracks" — directives stored in a note's frontmatter rather than inline YAML fences and HTML-comment target regions. The motivation is UX: the inline anatomy made notes feel like config, leaked into the editing surface, and competed with the writing flow. Frontmatter is invisible to the body editor, so moving directives there reclaims the body as just markdown the user wrote. The runtime agent now edits the note body freely via standard workspace tools rather than rewriting a constrained target region. Each track's instruction names an H2 section to own; the agent finds or creates that section, updates only its content, and self-heals position on subsequent runs. Triggers are now a unified array per track. cron / window / once / event in any combination, including multi-trigger setups (the flagship example: a priorities track that rebuilds at three day-windows and reacts to incoming gmail / calendar events). window is forgiving — fires once per day anywhere inside its band — so users opening the app late in the morning still get the morning run. The chip-in-editor is gone. Tracks are managed from a right-side sidebar opened by a Radio-icon button at the top-right of the editor toolbar. Cmd+K is no longer a Copilot entry point — search- only — pending a more intuitive invocation surface later. Today.md ships as the flagship demo of what tracks can do, with a versioned migration system so future template updates roll out cleanly to existing users (existing body preserved, old version backed up). Copilot is tuned to listen for any signal that the user wants something dynamic — not just the literal word "track". Strong phrasings get acted on directly; one-off questions about decaying information are answered first and then offered as a track. New or edited tracks run once by default so the user immediately sees content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4709e6eb89
commit
db6757514c
36 changed files with 2043 additions and 2275 deletions
415
apps/x/TRACKS.md
415
apps/x/TRACKS.md
|
|
@ -1,24 +1,29 @@
|
|||
# Track Blocks
|
||||
# Tracks
|
||||
|
||||
> Living blocks of content embedded in markdown notes that auto-refresh on a schedule, when relevant events arrive, or on demand.
|
||||
> Frontmatter directives that keep a markdown note's body auto-updated — on a schedule, when a relevant event arrives, or on demand.
|
||||
|
||||
A track block is a small YAML code fence plus a sibling HTML-comment region in a note. The YAML defines what to produce and when; the comment region holds the generated output, rewritten on each run. A single note can hold many independent tracks — one for weather, one for an email digest, one for a running project summary.
|
||||
A track is a single entry in a note's YAML frontmatter under the `track:` array. Each entry defines an instruction, optional triggers (cron / window / once / event — any mix), and (after the first run) some runtime state. When a trigger fires, a background agent edits the **note body** to satisfy the instruction. A note with no `track:` key is just a static note.
|
||||
|
||||
**Example** (a Chicago-time track refreshed hourly):
|
||||
**Example** (a note that shows the current Chicago time, refreshed hourly):
|
||||
|
||||
~~~markdown
|
||||
```track
|
||||
trackId: chicago-time
|
||||
instruction: Show the current time in Chicago, IL in 12-hour format.
|
||||
active: true
|
||||
schedule:
|
||||
type: cron
|
||||
expression: "0 * * * *"
|
||||
```
|
||||
---
|
||||
track:
|
||||
- id: chicago-time
|
||||
instruction: |
|
||||
Show the current time in Chicago, IL in 12-hour format.
|
||||
active: true
|
||||
triggers:
|
||||
- type: cron
|
||||
expression: "0 * * * *"
|
||||
lastRunAt: "2026-05-07T15:00:01.234Z"
|
||||
lastRunId: "..."
|
||||
lastRunSummary: "Updated — 3:00 PM, Central Time."
|
||||
---
|
||||
|
||||
<!--track-target:chicago-time-->
|
||||
2:30 PM, Central Time
|
||||
<!--/track-target:chicago-time-->
|
||||
# Chicago time
|
||||
|
||||
3:00 PM, Central Time
|
||||
~~~
|
||||
|
||||
## Table of Contents
|
||||
|
|
@ -27,279 +32,304 @@ schedule:
|
|||
2. [Architecture at a Glance](#architecture-at-a-glance)
|
||||
3. [Technical Flows](#technical-flows)
|
||||
4. [Schema Reference](#schema-reference)
|
||||
5. [Prompts Catalog](#prompts-catalog)
|
||||
6. [File Map](#file-map)
|
||||
7. [Known Follow-ups](#known-follow-ups)
|
||||
5. [Section Placement](#section-placement)
|
||||
6. [Daily-Note Template & Migrations](#daily-note-template--migrations)
|
||||
7. [Renderer UI](#renderer-ui)
|
||||
8. [Prompts Catalog](#prompts-catalog)
|
||||
9. [File Map](#file-map)
|
||||
|
||||
---
|
||||
|
||||
## Product Overview
|
||||
|
||||
### Trigger types
|
||||
### Triggers
|
||||
|
||||
A track block has exactly one of four trigger configurations. Schedule and event triggers can co-exist on a single track.
|
||||
A track has zero or more triggers under a single `triggers:` array. Each trigger is one of four types and can be mixed freely:
|
||||
|
||||
| Trigger | When it fires | How to express it |
|
||||
| Type | When it fires | Shape |
|
||||
|---|---|---|
|
||||
| **Manual** | Only when the user (or Copilot) hits Run | Omit `schedule`, leave `eventMatchCriteria` unset |
|
||||
| **Scheduled — cron** | At exact cron times | `schedule: { type: cron, expression: "0 * * * *" }` |
|
||||
| **Scheduled — window** | At most once per cron occurrence, only within a time-of-day window | `schedule: { type: window, cron, startTime: "09:00", endTime: "17:00" }` |
|
||||
| **Scheduled — once** | Once at a future time, then never | `schedule: { type: once, runAt: "2026-04-14T09:00:00" }` |
|
||||
| **Event-driven** | When a matching event arrives (e.g. new Gmail thread) | `eventMatchCriteria: "Emails about Q3 planning"` |
|
||||
| **`cron`** | At exact cron times | `{ type: cron, expression: "0 * * * *" }` |
|
||||
| **`window`** | Once per day, anywhere inside a time-of-day band | `{ type: window, startTime: "09:00", endTime: "12:00" }` |
|
||||
| **`once`** | Once at a future time, then never | `{ type: once, runAt: "2026-04-14T09:00:00" }` |
|
||||
| **`event`** | When a matching event arrives (e.g. new Gmail thread) | `{ type: event, matchCriteria: "Emails about Q3 planning" }` |
|
||||
|
||||
Decision shorthand: cron for exact times, window for "sometime in the morning", once for one-shots, event for signal-driven updates. Combine `schedule` + `eventMatchCriteria` for a track that refreshes on a cadence **and** reacts to incoming signals.
|
||||
A track with no `triggers` (or an empty array) is **manual-only** — fires only when the user clicks Run in the sidebar.
|
||||
|
||||
`cron` and `once` enforce a 2-minute grace window — if the scheduled time passed more than 2 minutes ago (e.g. the app was offline), the run is skipped, not replayed. `window` is forgiving by design: as long as it's still inside the band and the day's cycle hasn't fired yet, it fires the moment the app is open. The day's cycle is anchored at `startTime`.
|
||||
|
||||
A single track can carry multiple triggers. The flagship example is in Today.md's `priorities` track: three `window` entries (morning / midday / post-lunch) plus two `event` entries (gmail / calendar) — five triggers total, giving a baseline rebuild three times per day plus reactive updates on incoming signals.
|
||||
|
||||
### Creating a track
|
||||
|
||||
Three paths, all produce identical on-disk YAML:
|
||||
Two paths, both producing identical on-disk YAML:
|
||||
|
||||
1. **Hand-written** — type the YAML fence directly in a note. The renderer picks it up instantly via the Tiptap `track-block` extension.
|
||||
2. **Cmd+K with cursor context** — press Cmd+K anywhere in a note, say "add a track that shows Chicago weather hourly". Copilot loads the `tracks` skill and splices the block at the cursor using `workspace-edit`.
|
||||
3. **Sidebar chat** — mention a note (or have it attached) and ask for a track. Copilot appends the block at the end or under the heading you name.
|
||||
1. **Hand-written** — type the entry directly into a note's frontmatter under `track:`. The scheduler picks it up on its next 15-second tick.
|
||||
2. **Sidebar chat** — mention a note (or have it attached) and ask Copilot for something dynamic. Copilot is tuned to recognize a wide range of phrasings beyond the literal word "track" (see "Prompts Catalog → Copilot trigger paragraph" for the signal taxonomy); it loads the `tracks` skill, edits the note's frontmatter via `workspace-edit`, then **runs the track once by default** so the user immediately sees content. The auto-run is skipped only when the user explicitly says not to run yet.
|
||||
|
||||
### Viewing and managing a track
|
||||
There is no inline-block creation flow anymore. The Cmd+K palette is search-only and does not invoke Copilot.
|
||||
|
||||
The editor shows track blocks as an inline **chip** — a pill with the track ID, a clipped instruction, a color rail matching the trigger type, and a spinner while running.
|
||||
### Viewing and managing tracks
|
||||
|
||||
Clicking the chip opens the **track modal**, where everything happens:
|
||||
The editor has a Radio-icon button in the top toolbar (right side) that opens the **Track Sidebar** for the current note. The sidebar:
|
||||
|
||||
- **Header** — track ID, schedule summary (e.g. "Hourly"), and a Switch to pause/resume (flips `active`).
|
||||
- **Tabs** — *What to track* (renders `instruction` as markdown), *When to run* (schedule details, shown if `schedule` is present), *Event matching* (shown if `eventMatchCriteria` is present), *Details* (ID, file, run metadata).
|
||||
- **Advanced** — expandable raw-YAML editor for power users.
|
||||
- **Danger zone** (Details tab) — delete with two-step confirmation; also removes the target region.
|
||||
- **Footer** — *Edit with Copilot* (opens sidebar chat with the note attached) and *Run now* (triggers immediately).
|
||||
- **List view** — one row per track in the note's frontmatter. Title is the track's `id`; subtitle is the trigger summary plus a `Paused ·` prefix when applicable, plus the instruction's first line as a tertiary line. A Play button on the right runs that track.
|
||||
- **Detail view** (click a row) — back arrow + tabs (*What* / *Schedule* / *Events* / *Details*), an advanced raw-YAML editor, danger-zone delete, and a footer with "Edit with Copilot" + "Run now".
|
||||
- **Status hook** — `useTrackStatus` subscribes to `tracks:events` IPC; rows show a spinner whenever a track is running, regardless of hover state.
|
||||
|
||||
Every mutation inside the modal goes through IPC to the backend — the renderer never writes the file itself. This is the fix that prevents the editor's save cycle from clobbering backend-written fields like `lastRunAt`.
|
||||
Every mutation in the sidebar goes through IPC to the backend — the renderer never writes the file itself. This avoids races with the scheduler/runner writing runtime fields like `lastRunAt`.
|
||||
|
||||
### What Copilot can do
|
||||
### What the runtime agent does
|
||||
|
||||
- **Create, edit, and delete** track blocks (via the `tracks` skill + `workspace-edit` / `workspace-readFile`).
|
||||
- **Run** a track with the `run-track-block` builtin tool. An optional `context` parameter biases this single run — e.g. `"Backfill from gmail_sync/ for the last 90 days about this topic"`. Common use: seed a newly-created event-driven track so its target region isn't empty waiting for the next matching event.
|
||||
- **Proactively suggest** tracks when the user signals interest ("track", "monitor", "watch", "every morning"), per the trigger paragraph in `instructions.ts`.
|
||||
- **Re-enter edit mode** via the modal's *Edit with Copilot* button, which seeds a new chat with the note attached and invites Copilot to load the `tracks` skill.
|
||||
When a trigger fires, a background agent ("track-run") receives a short message:
|
||||
- The track's `id`, the workspace-relative path to the note, and a localized timestamp.
|
||||
- The instruction.
|
||||
- For event runs only: the matching `eventMatchCriteria` text and the event payload, with a Pass-2 decision directive ("only edit if the event genuinely warrants it").
|
||||
|
||||
### After a run
|
||||
The agent's system prompt tells it to:
|
||||
1. Call `workspace-readFile` to read the current note (the body may be long; no body snapshot is passed in the message — fetch fresh).
|
||||
2. Find or create the H2 section the instruction names (placement model below).
|
||||
3. Update only that section's content. Never modify YAML frontmatter — that's owned by the user and the runtime.
|
||||
4. After writing, re-check its section's position; cut-and-paste only its own block if it's misplaced (handles the cold-start firing-order problem).
|
||||
5. End with a one-line summary stored as `lastRunSummary`.
|
||||
|
||||
- The **target region** (between `<!--track-target:ID-->` markers) is rewritten by the track-run agent using the `update-track-content` tool.
|
||||
- `lastRunAt`, `lastRunId`, `lastRunSummary` are updated in the YAML.
|
||||
- The chip pulses while running, then displays the latest `lastRunAt`.
|
||||
- Bus events (`track_run_start` / `track_run_complete`) are forwarded to the renderer via the `tracks:events` IPC channel, consumed by the `useTrackStatus` hook.
|
||||
The agent has the full workspace toolkit (read/edit/grep/web-search/browser/MCP) — there's no special "track-content" tool anymore; tracks just ship general edits.
|
||||
|
||||
---
|
||||
|
||||
## Architecture at a Glance
|
||||
|
||||
```
|
||||
Editor chip (display-only) ──click──► TrackModal (React)
|
||||
│
|
||||
├──► IPC: track:get / update /
|
||||
│ replaceYaml / delete / run
|
||||
│
|
||||
Editor toolbar Radio button ─click──► TrackSidebar (React)
|
||||
│
|
||||
├──► IPC: track:get / update /
|
||||
│ replaceYaml / delete / run
|
||||
│
|
||||
Backend (main process)
|
||||
├─ Scheduler loop (15 s) ──┐
|
||||
├─ Event processor (5 s) ──┼──► triggerTrackUpdate() ──► track-run agent
|
||||
└─ Copilot tool run-track-block ──┘ │
|
||||
▼
|
||||
update-track-content tool
|
||||
│
|
||||
▼
|
||||
target region rewritten on disk
|
||||
├─ Event processor (5 s) ──┼──► triggerTrackUpdate() ──► track-run agent
|
||||
└─ Builtin tool run-track ─┘ │
|
||||
▼
|
||||
workspace-readFile / -edit
|
||||
│
|
||||
▼
|
||||
body region rewritten on disk
|
||||
frontmatter lastRun* patched
|
||||
```
|
||||
|
||||
**Single-writer invariant:** the renderer is never a file writer. All on-disk changes go through backend helpers in `packages/core/src/knowledge/track/fileops.ts` (`updateTrackBlock`, `replaceTrackBlockYaml`, `deleteTrackBlock`). This avoids races with the scheduler/runner writing runtime fields.
|
||||
**Single-writer invariant** — the renderer is never a file writer for the `track:` key. All on-disk changes go through backend helpers in `packages/core/src/knowledge/track/fileops.ts` (`updateTrack`, `replaceTrackYaml`, `deleteTrack`). `extractAllFrontmatterValues` in the renderer's frontmatter helper explicitly skips the `track:` key (and `buildFrontmatter` splices it back from the original raw on save), so the FrontmatterProperties UI can't accidentally mangle it.
|
||||
|
||||
**Event contract:** `window.dispatchEvent(CustomEvent('rowboat:open-track-modal', { detail }))` is the sole entry point from editor chip → modal. Similarly, `rowboat:open-copilot-edit-track` opens the sidebar chat with context.
|
||||
**Event contract** — `window.dispatchEvent(CustomEvent('rowboat:open-track-sidebar', { detail: { filePath } }))` is the sole entry point from editor toolbar → sidebar. `rowboat:open-copilot-edit-track` opens the Copilot sidebar with the note attached.
|
||||
|
||||
---
|
||||
|
||||
## Technical Flows
|
||||
|
||||
### 4.1 Scheduling (cron / window / once)
|
||||
### Scheduling (cron / window / once)
|
||||
|
||||
- Module: `packages/core/src/knowledge/track/scheduler.ts`. Polls every **15 seconds** (`POLL_INTERVAL_MS`).
|
||||
- Each tick: `workspace.readdir('knowledge', { recursive: true })`, filter `.md`, iterate all track blocks via `fetchAll(relPath)`.
|
||||
- Per-track due check: `isTrackScheduleDue(schedule, lastRunAt)` in `schedule-utils.ts`. All three schedule types enforce a **2-minute grace window** — missed schedules (app offline at trigger time) are skipped, not replayed.
|
||||
- When due, fires `triggerTrackUpdate(trackId, filePath, undefined, 'timed')` (fire-and-forget; the runner's concurrency guard prevents duplicates).
|
||||
- Startup: `initTrackScheduler()` is called in `apps/main/src/main.ts` at app-ready, alongside `initTrackEventProcessor()`.
|
||||
- **Module**: `packages/core/src/knowledge/track/scheduler.ts`. Polls every **15 seconds** (`POLL_INTERVAL_MS`).
|
||||
- Each tick: `workspace.readdir('knowledge', { recursive: true })`, filter `.md`, iterate all tracks via `fetchAll(relPath)`.
|
||||
- For each track with `active === true` and at least one timed trigger (`cron` / `window` / `once`), `find` the first due trigger via `isTriggerDue(t, lastRunAt)` (`schedule-utils.ts`).
|
||||
- When due, fire `triggerTrackUpdate(track.id, relPath, undefined, 'timed')` (fire-and-forget; the runner's concurrency guard prevents duplicates).
|
||||
- **Grace window** — `cron` and `once` enforce a 2-minute grace; missed schedules are skipped, not replayed. `window` has no grace — anywhere inside the band counts.
|
||||
- **Window cycle anchor** — a window's daily cycle starts at `startTime`. Once a fire lands strictly after today's `startTime`, that window is done for the day. The strict comparison handles the boundary case (e.g. an 08:00–12:00 + a 12:00–15:00 window each get to fire even when the morning fire happens exactly at 12:00:00).
|
||||
- **Startup** — `initTrackScheduler()` is called in `apps/main/src/main.ts` at app-ready, alongside `initTrackEventProcessor()`.
|
||||
|
||||
### 4.2 Event pipeline
|
||||
### Event pipeline
|
||||
|
||||
**Producers** — any data source that should feed tracks emits events:
|
||||
|
||||
- **Gmail** (`packages/core/src/knowledge/sync_gmail.ts`) — three call sites, each after a successful thread sync, call `createEvent({ source: 'gmail', type: 'email.synced', payload: <thread markdown> })`.
|
||||
- **Calendar** (`packages/core/src/knowledge/sync_calendar.ts`) — one bundled event per sync. The payload is a markdown digest of all new/updated/deleted events built by `summarizeCalendarSync()` (line 68). `publishCalendarSyncEvent()` (line ~126) wraps it with `source: 'calendar'`, `type: 'calendar.synced'`.
|
||||
- **Gmail** (`packages/core/src/knowledge/sync_gmail.ts`) — call sites after a successful thread sync invoke `createEvent({ source: 'gmail', type: 'email.synced', payload: <thread markdown> })`.
|
||||
- **Calendar** (`packages/core/src/knowledge/sync_calendar.ts`) — one bundled event per sync, with a markdown digest payload built by `summarizeCalendarSync()`.
|
||||
|
||||
**Storage** — `packages/core/src/knowledge/track/events.ts` writes each event as a JSON file under `~/.rowboat/events/pending/<id>.json`. IDs come from the DI-resolved `IdGen` (ISO-based, lexicographically sortable) — so `readdirSync(...).sort()` is strict FIFO.
|
||||
|
||||
**Consumer loop** — same file, `init()` polls every **5 seconds**, then `processPendingEvents()` walks sorted filenames. For each event:
|
||||
|
||||
1. Parse via `KnowledgeEventSchema`; malformed files go to `done/` with `error` set (the loop stays alive).
|
||||
2. `listAllTracks()` scans every `.md` under `knowledge/` and collects `ParsedTrack[]`.
|
||||
3. `findCandidates(event, allTracks)` in `routing.ts` runs Pass 1 LLM routing (below).
|
||||
4. For each candidate, `triggerTrackUpdate(trackId, filePath, event.payload, 'event')` **sequentially** — preserves total ordering within the event.
|
||||
5. Enrich the event JSON with `processedAt`, `candidates`, `runIds`, `error?`, then `moveEventToDone()` — write to `events/done/<id>.json`, unlink from `pending/`.
|
||||
2. `listAllTracks()` scans every `.md` under `knowledge/`. Only tracks with at least one `event`-type trigger appear in the routing list; their `eventMatchCriteria` is the joined `matchCriteria` from all event triggers (`'; '`-separated).
|
||||
3. `findCandidates(event, allTracks)` runs Pass 1 LLM routing (below).
|
||||
4. For each candidate, `triggerTrackUpdate(id, filePath, event.payload, 'event')` **sequentially** — preserves total ordering within the event.
|
||||
5. Enrich the event JSON with `processedAt`, `candidates`, `runIds`, `error?`, then move to `events/done/<id>.json`.
|
||||
|
||||
**Pass 1 routing** (`routing.ts:73+ findCandidates`):
|
||||
|
||||
- **Short-circuit**: if `event.targetTrackId` + `targetFilePath` are set (manual re-run events), skip the LLM and return that track directly.
|
||||
**Pass 1 routing** (`routing.ts`):
|
||||
- **Short-circuit** — if `event.targetTrackId` + `event.targetFilePath` are set (manual re-run events), skip the LLM and return that track directly.
|
||||
- Filter to `active && instruction && eventMatchCriteria` tracks.
|
||||
- Batches of `BATCH_SIZE = 20`.
|
||||
- Per batch, `generateObject()` with `ROUTING_SYSTEM_PROMPT` + `buildRoutingPrompt()` and `Pass1OutputSchema` → `candidates: { trackId, filePath }[]`. The composite key `trackId::filePath` deduplicates across files since `trackId` is only unique per file.
|
||||
- Model resolution: gateway if signed in, local provider otherwise; prefers `knowledgeGraphModel` from config.
|
||||
- Per batch, `generateObject()` with `ROUTING_SYSTEM_PROMPT` + `buildRoutingPrompt()` and `Pass1OutputSchema` → `candidates: { trackId, filePath }[]`. The composite key `trackId::filePath` deduplicates across files since `id` is only unique per file.
|
||||
|
||||
**Pass 2 decision** happens inside the track-run agent (see Run flow below) — the liberal Pass 1 produces candidates, the agent vetoes false positives before touching the target region.
|
||||
**Pass 2 decision** happens inside the track-run agent (see Run flow) — Pass 1 is liberal, the agent vetoes false positives before touching the body.
|
||||
|
||||
### 4.3 Run flow (`triggerTrackUpdate`)
|
||||
### Run flow (`triggerTrackUpdate`)
|
||||
|
||||
Module: `packages/core/src/knowledge/track/runner.ts`.
|
||||
|
||||
1. **Concurrency guard** — static `runningTracks: Set<string>` keyed by `${trackId}:${filePath}`. Duplicate calls return `{ action: 'no_update', error: 'Already running' }`.
|
||||
2. **Fetch block** via `fetchAll(filePath)`, locate by `trackId`.
|
||||
3. **Create agent run** — `createRun({ agentId: 'track-run' })`.
|
||||
4. **Set `lastRunAt` + `lastRunId` immediately** (before the agent executes). Prevents retry storms: if the run fails, the scheduler's next tick won't re-trigger, and for `once` tracks the "done" flag is already set.
|
||||
5. **Emit `track_run_start`** on the `trackBus` with the trigger type (`manual` / `timed` / `event`).
|
||||
6. **Send agent message** built by `buildMessage(filePath, track, trigger, context?)` — three branches (see Prompts Catalog). For `'event'` trigger, the message includes the event payload and a Pass 2 decision directive.
|
||||
7. **Wait for completion** — `waitForRunCompletion(runId)`, then `extractAgentResponse(runId)` for the summary.
|
||||
8. **Compare content**: re-read the file, diff `contentBefore` vs `contentAfter`. If changed → `action: 'replace'`; else → `action: 'no_update'`.
|
||||
9. **Store `lastRunSummary`** via `updateTrackBlock`.
|
||||
10. **Emit `track_run_complete`** with `summary` or `error`.
|
||||
11. **Cleanup**: `runningTracks.delete(key)` in a `finally` block.
|
||||
1. **Concurrency guard** — static `runningTracks: Set<string>` keyed by `${id}:${filePath}`. Duplicate calls return `{ action: 'no_update', error: 'Already running' }`.
|
||||
2. **Fetch track** via `fetchAll(filePath)`, locate by `id`.
|
||||
3. **Snapshot body** via `readNoteBody(filePath)` for the post-run diff.
|
||||
4. **Create agent run** — `createRun({ agentId: 'track-run' })`.
|
||||
5. **Set `lastRunAt` + `lastRunId` immediately** (before the agent executes). Prevents retry storms: if the run fails, the scheduler's next tick won't re-trigger; for `once` tracks the "done" marker is already set.
|
||||
6. **Emit `track_run_start`** on the `trackBus` with the trigger type (`manual` / `timed` / `event`).
|
||||
7. **Send agent message** built by `buildMessage(filePath, track, trigger, context?)` (see Prompts Catalog #4). The path is converted to its workspace-relative form (`knowledge/${filePath}`) so the agent's tools resolve correctly.
|
||||
8. **Wait for completion** — `waitForRunCompletion(runId)`, then `extractAgentResponse(runId)` for the summary.
|
||||
9. **Compare body**: re-read body via `readNoteBody(filePath)`, diff vs the snapshot. If changed → `action: 'replace'`; else → `action: 'no_update'`.
|
||||
10. **Patch `lastRunSummary`** via `updateTrack(filePath, id, { lastRunSummary })`.
|
||||
11. **Emit `track_run_complete`** with `summary` or `error`.
|
||||
12. **Cleanup**: `runningTracks.delete(key)` in a `finally` block.
|
||||
|
||||
Returned to callers: `{ trackId, runId, action, contentBefore, contentAfter, summary, error? }`.
|
||||
|
||||
### 4.4 IPC surface
|
||||
### IPC surface
|
||||
|
||||
| Channel | Caller → handler | Purpose |
|
||||
|---|---|---|
|
||||
| `track:run` | Renderer (chip/modal Run button) | Fires `triggerTrackUpdate(..., 'manual')` |
|
||||
| `track:get` | Modal on open | Returns fresh YAML from disk via `fetchYaml` |
|
||||
| `track:update` | Modal toggle / partial edits | `updateTrackBlock` merges a partial into on-disk YAML |
|
||||
| `track:replaceYaml` | Advanced raw-YAML save | `replaceTrackBlockYaml` validates + writes full YAML |
|
||||
| `track:delete` | Danger-zone confirm | `deleteTrackBlock` removes YAML fence + target region |
|
||||
| `tracks:events` | Server → renderer (`webContents.send`) | Forwards `trackBus` events to the `useTrackStatus` hook |
|
||||
| `track:run` | Renderer (sidebar Run button) | Fires `triggerTrackUpdate(..., 'manual')` |
|
||||
| `track:get` | Sidebar on detail open | Returns fresh per-track YAML from disk via `fetchYaml(filePath, id)` |
|
||||
| `track:update` | Sidebar toggle / partial edits | `updateTrack` merges a partial into the on-disk entry |
|
||||
| `track:replaceYaml` | Sidebar advanced raw-YAML save | `replaceTrackYaml` validates + writes the full entry |
|
||||
| `track:delete` | Sidebar danger-zone confirm | `deleteTrack` removes the entry from the `track:` array |
|
||||
| `track:setNoteActive` | Background-agents view toggle | Flips `active` on every track in a note |
|
||||
| `track:listNotes` | Background-agents view load | Lists all notes that contain at least one track, with summary fields |
|
||||
| `tracks:events` | Server → renderer (`webContents.send`) | Forwards `trackBus` events to `useTrackStatus` |
|
||||
|
||||
Request/response schemas live in `packages/shared/src/ipc.ts`; handlers in `apps/main/src/ipc.ts`; backend helpers in `packages/core/src/knowledge/track/fileops.ts`.
|
||||
|
||||
### 4.5 Renderer integration
|
||||
### Concurrency & FIFO guarantees
|
||||
|
||||
- **Chip** — `apps/renderer/src/extensions/track-block.tsx`. Tiptap atom node. Render-only: click dispatches `CustomEvent('rowboat:open-track-modal', { detail: { trackId, filePath, initialYaml, onDeleted } })`. The `onDeleted` callback is the Tiptap `deleteNode` — the modal calls it after a successful `track:delete` IPC so the editor also drops the node and doesn't resurrect it on next save.
|
||||
- **Modal** — `apps/renderer/src/components/track-modal.tsx`. Mounted once in `App.tsx`, self-listens for the open event. On open: calls `track:get` to get the authoritative YAML, not the Tiptap cached attr. All mutations go through IPC; `updateAttributes` is never called.
|
||||
- **Status hook** — `apps/renderer/src/hooks/use-track-status.ts`. Subscribes to `tracks:events` and maintains a `Map<"${trackId}:${filePath}", RunState>` so chips and the modal reflect live run state.
|
||||
- **Edit with Copilot** — the modal's footer button dispatches `rowboat:open-copilot-edit-track`. An `useEffect` listener in `App.tsx` opens the chat sidebar via `submitFromPalette(text, mention)`, where `text` is a short opener that invites Copilot to load the `tracks` skill and `mention` attaches the note file.
|
||||
|
||||
### 4.6 Copilot skill integration
|
||||
|
||||
- **Skill content** — `packages/core/src/application/assistant/skills/tracks/skill.ts` (~318 lines). Exported as a `String.raw` template literal; injected into the Copilot system prompt when the `loadSkill('tracks')` tool is called.
|
||||
- **Canonical schema** inside the skill is **auto-generated at module load**: `stringifyYaml(z.toJSONSchema(TrackBlockSchema))`. Editing `TrackBlockSchema` in `packages/shared/src/track-block.ts` propagates to the skill without manual sync.
|
||||
- **Skill registration** — `packages/core/src/application/assistant/skills/index.ts` (import + entry in the `definitions` array).
|
||||
- **Loading trigger** — `packages/core/src/application/assistant/instructions.ts:73` — one paragraph tells Copilot to load the skill on track / monitor / watch / "keep an eye on" / Cmd+K auto-refresh requests.
|
||||
- **Builtin tools** — `packages/core/src/application/lib/builtin-tools.ts`:
|
||||
- `update-track-content` — low-level: rewrite the target region between `<!--track-target:ID-->` markers. Used mainly by the track-run agent.
|
||||
- `run-track-block` — high-level: trigger a run with an optional `context`. Uses `await import("../../knowledge/track/runner.js")` inside the execute body to break a module-init cycle (`builtin-tools → track/runner → runs/runs → agents/runtime → builtin-tools`).
|
||||
|
||||
### 4.7 Concurrency & FIFO guarantees
|
||||
|
||||
- **Per-track serialization** — the `runningTracks` guard in `runner.ts`. A track is at most running once; overlapping triggers (manual + scheduled + event) return `error: 'Already running'` cleanly through IPC.
|
||||
- **Backend is single writer** — renderer uses IPC for every edit; backend helpers in `fileops.ts` are the only code paths that touch the file.
|
||||
- **Event FIFO** — monotonic `IdGen` IDs → lexicographic filenames → `sort()` in `processPendingEvents()` = strict FIFO across events. Candidates within one event are processed sequentially (`await` in loop) so ordering holds within an event too.
|
||||
- **Per-track serialization** — the `runningTracks` guard in `runner.ts`. A track is at most running once at a time; overlapping triggers (manual + scheduled + event) return `error: 'Already running'`.
|
||||
- **Backend is single writer for `track:`** — all editing goes through fileops; the renderer's FrontmatterProperties UI explicitly preserves `track:` byte-for-byte across saves.
|
||||
- **File lock** — every fileops mutation runs under `withFileLock(absPath)` so the runner, scheduler, and IPC handlers serialize on the file.
|
||||
- **Event FIFO** — monotonic `IdGen` IDs → lexicographic filenames → `sort()` in `processPendingEvents()`. Candidates within one event are processed sequentially.
|
||||
- **No retry storms** — `lastRunAt` is set at the *start* of a run, not the end. A crash mid-run leaves the track marked as ran; the scheduler's next tick computes the next occurrence from that point.
|
||||
|
||||
---
|
||||
|
||||
## Schema Reference
|
||||
|
||||
All canonical schemas live in `packages/shared/src/track-block.ts`:
|
||||
All canonical schemas live in `packages/shared/src/track.ts`:
|
||||
|
||||
- `TrackBlockSchema` — the YAML that goes inside a ` ```track ` fence. User-authored fields: `trackId` (kebab-case, unique within the file), `instruction`, `active`, `schedule?`, `eventMatchCriteria?`. **Runtime-managed fields (never hand-write):** `lastRunAt`, `lastRunId`, `lastRunSummary`.
|
||||
- `TrackScheduleSchema` — discriminated union over `{ type: 'cron' | 'window' | 'once' }`.
|
||||
- `TrackSchema` — a single entry in the frontmatter `track:` array. Fields: `id` (kebab-case, unique within the note), `instruction`, `active` (default true), `triggers?`, `model?`, `provider?`, `icon?`. **Runtime-managed (never hand-write):** `lastRunAt`, `lastRunId`, `lastRunSummary`.
|
||||
- `TriggerSchema` — discriminated union over `{ type: 'cron' | 'window' | 'once' | 'event' }`. Window has just `startTime` + `endTime` (no `cron` field — the cycle is anchored at `startTime`).
|
||||
- `KnowledgeEventSchema` — the on-disk shape of each event JSON in `events/pending/` and `events/done/`. Enrichment fields (`processedAt`, `candidates`, `runIds`, `error`) are populated when moving to `done/`.
|
||||
- `Pass1OutputSchema` — the structured response the routing classifier returns: `{ candidates: { trackId, filePath }[] }`.
|
||||
|
||||
Since the skill's schema block is generated from `TrackBlockSchema`, schema changes should start here — the skill, the validator in `replaceTrackBlockYaml`, and modal display logic all follow from the Zod source of truth.
|
||||
The skill's Canonical Schema block is auto-generated at module load — `stringifyYaml(z.toJSONSchema(TrackSchema))` — so editing `TrackSchema` propagates to the skill on the next build.
|
||||
|
||||
---
|
||||
|
||||
## Section Placement
|
||||
|
||||
Tracks no longer have formal target regions. Each instruction names a section by H2 heading (e.g. *"in a section titled 'Overview' at the top"*) and the agent finds or creates that section.
|
||||
|
||||
The contract (defined in the run-agent system prompt — `packages/core/src/knowledge/track/run-agent.ts`):
|
||||
|
||||
- Sections are **H2 headings** (`## Section Name`). Match by exact heading text.
|
||||
- **Existing**: replace its content (everything between that heading and the next H2 — or end of file). Heading itself stays.
|
||||
- **Missing**: create it. The placement hint determines location:
|
||||
- "at the top" → just below the H1 title.
|
||||
- "after X" → immediately after section X.
|
||||
- no hint → append.
|
||||
- **Self-heal**: after writing, the agent re-checks its section's position. If misplaced (the cold-start case where empty notes get sections in firing order rather than reading order), the agent moves only its **own** H2 block — never reorders other tracks' sections.
|
||||
- **Boundaries**: never modify another track's section content; never duplicate; never touch frontmatter; if the user renamed the heading, recreate per the placement hint.
|
||||
|
||||
This keeps tracks loosely coupled: each one stakes out a section by name, and the rest of the body is entirely the user's.
|
||||
|
||||
---
|
||||
|
||||
## Daily-Note Template & Migrations
|
||||
|
||||
`Today.md` is the canonical demo of what tracks can do. It ships with six tracks (overview/photo combined into one, calendar, emails, what-you-missed, priorities) showing pure-cron, pure-event, multi-window, and multi-trigger configurations.
|
||||
|
||||
**Versioning** — `packages/core/src/knowledge/ensure_daily_note.ts` carries a `CANONICAL_DAILY_NOTE_VERSION` constant and a `templateVersion` scalar in the frontmatter. On app start, `ensureDailyNote()`:
|
||||
|
||||
- File missing → fresh write at canonical version.
|
||||
- File at-or-above canonical → no-op.
|
||||
- File below canonical → rename existing to `Today.md.bkp.<ISO-stamp>` (which doesn't end in `.md`, so the scheduler/event router skip it), then write the canonical template with the body byte-preserved via `splitFrontmatter` from `application/lib/parse-frontmatter.ts`.
|
||||
|
||||
Any change to the canonical TRACKS list, instructions, default body, or trigger config should bump the constant. Existing users will get the new template on next launch with their body sections preserved; their `lastRunAt` and any custom additions to the tracks list are dropped (the .bkp file is the recovery path).
|
||||
|
||||
---
|
||||
|
||||
## Renderer UI
|
||||
|
||||
The chip-in-editor model is gone. Replacements:
|
||||
|
||||
- **Toolbar button** — `apps/renderer/src/components/editor-toolbar.tsx`. A Radio-icon ghost button at the top-right of the editor toolbar. `markdown-editor.tsx` passes `onOpenTracks` (only when a `notePath` is available) which dispatches `rowboat:open-track-sidebar` with `{ filePath }`.
|
||||
- **Sidebar** — `apps/renderer/src/components/track-sidebar.tsx`. Right-anchored, mounted once in `App.tsx`. Self-listens for `rowboat:open-track-sidebar`; on open, calls `workspace:readFile` and parses tracks from the frontmatter on the renderer side (uses the same `TrackSchema` from `@x/shared`). All mutations go through IPC.
|
||||
- Constant top header: Radio icon, "Tracks" title, note name subtitle, X close. Uses the `bg-sidebar` design tokens to match the app's left sidebar.
|
||||
- List view: one row per track. Title is `id`; subtitle is the trigger summary (with `Paused ·` prefix); third line is the instruction's first line, truncated. Run button always visible while running, otherwise fades in on hover.
|
||||
- Detail view: back arrow + track id; status row (trigger summary + Active/Paused toggle); tabs (`What` / `Schedule` / `Events` / `Details`); advanced raw-YAML editor; danger-zone delete; footer (Edit with Copilot + Run now).
|
||||
- **Status hook** — `apps/renderer/src/hooks/use-track-status.ts`. Subscribes to `tracks:events` IPC and maintains a `Map<"${id}:${filePath}", RunState>` keyed by composite key.
|
||||
- **Edit-with-Copilot flow** — sidebar dispatches `rowboat:open-copilot-edit-track` (App.tsx listener handles it via `submitFromPalette`).
|
||||
- **FrontmatterProperties safety** — `apps/renderer/src/lib/frontmatter.ts` adds `STRUCTURED_KEYS = new Set(['track'])`. `extractAllFrontmatterValues` filters those keys out (so they never appear in the editable property list), and `buildFrontmatter(fields, preserveRaw)` splices the original `track:` block back from `preserveRaw` on save. This means the property panel can edit `tags` / `status` / etc. without ever clobbering the tracks frontmatter.
|
||||
|
||||
---
|
||||
|
||||
## Prompts Catalog
|
||||
|
||||
Every LLM-facing prompt in the feature, with file + line pointers so you can edit in place. After any edit: `cd apps/x && npm run deps` to rebuild the affected package, then restart the app (`npm run dev`).
|
||||
Every LLM-facing prompt in the feature, with file pointers so you can edit in place. After any edit: `cd apps/x && npm run deps` to rebuild the affected package, then restart the app.
|
||||
|
||||
### 1. Routing system prompt (Pass 1 classifier)
|
||||
|
||||
- **Purpose**: decide which tracks *might* be relevant to an incoming event. Liberal — prefers false positives; Pass 2 inside the agent catches them.
|
||||
- **File**: `packages/core/src/knowledge/track/routing.ts:22–37` (`ROUTING_SYSTEM_PROMPT`).
|
||||
- **Inputs**: none interpolated — constant system prompt.
|
||||
- **Purpose**: decide which tracks *might* be relevant to an incoming event. Liberal — prefers false positives; the run-agent does Pass 2.
|
||||
- **File**: `packages/core/src/knowledge/track/routing.ts` (`ROUTING_SYSTEM_PROMPT`).
|
||||
- **Output**: structured `Pass1OutputSchema` — `{ candidates: { trackId, filePath }[] }`.
|
||||
- **Invoked by**: `findCandidates()` at `routing.ts:73+`, per batch of 20 tracks, via `generateObject({ model, system, prompt, schema })`.
|
||||
- **Invoked by**: `findCandidates()` per batch of 20 tracks via `generateObject({ model, system, prompt, schema })`.
|
||||
|
||||
### 2. Routing user prompt template
|
||||
|
||||
- **Purpose**: formats the event and the current batch of tracks into the user message fed alongside the system prompt.
|
||||
- **File**: `packages/core/src/knowledge/track/routing.ts:51–66` (`buildRoutingPrompt`).
|
||||
- **Inputs**: `event` (source, type, createdAt, payload), `batch: ParsedTrack[]` (each: `trackId`, `filePath`, `eventMatchCriteria`).
|
||||
- **Output**: plain text, two sections — `## Event` and `## Track Blocks`.
|
||||
- **Note**: the event `payload` is what the producer wrote. For Gmail it's a thread markdown dump; for Calendar it's a digest (see #7 below).
|
||||
- **Purpose**: formats the event and the current batch of tracks into the user message for Pass 1.
|
||||
- **File**: `packages/core/src/knowledge/track/routing.ts` (`buildRoutingPrompt`).
|
||||
- **Inputs**: `event` (source, type, createdAt, payload), `batch: ParsedTrack[]` (each: `trackId`, `filePath`, `matchCriteria` — joined from all event triggers, `'; '`-separated).
|
||||
- **Output**: plain text, two sections — `## Event` and `## Tracks`.
|
||||
|
||||
### 3. Track-run agent instructions
|
||||
|
||||
- **Purpose**: system prompt for the background agent that actually rewrites target regions. Sets tone ("no user present — don't ask questions"), points at the knowledge graph, and prescribes the `update-track-content` tool as the write path.
|
||||
- **File**: `packages/core/src/knowledge/track/run-agent.ts:6–50` (`TRACK_RUN_INSTRUCTIONS`).
|
||||
- **Inputs**: `${WorkDir}` template literal (substituted at module load).
|
||||
- **Purpose**: system prompt for the background agent that rewrites note bodies. Sets tone, defines the section-placement contract (find/create/self-heal), points at the knowledge graph, and prescribes general `workspace-readFile` / `workspace-edit` as the write path.
|
||||
- **File**: `packages/core/src/knowledge/track/run-agent.ts` (`TRACK_RUN_INSTRUCTIONS`).
|
||||
- **Inputs**: `${WorkDir}` template literal substituted at module load.
|
||||
- **Output**: free-form — agent calls tools, ends with a 1-2 sentence summary used as `lastRunSummary`.
|
||||
- **Invoked by**: `buildTrackRunAgent()` at line 52, called during agent runtime setup. Tool set = all `BuiltinTools` except `executeCommand`.
|
||||
- **Invoked by**: `buildTrackRunAgent()`, called during agent runtime setup. Tool set = all `BuiltinTools` except `executeCommand`.
|
||||
|
||||
### 4. Track-run agent message (`buildMessage`)
|
||||
|
||||
- **Purpose**: the user message seeded into each track-run. Three shape variants based on `trigger`.
|
||||
- **File**: `packages/core/src/knowledge/track/runner.ts:23–62`.
|
||||
- **Inputs**: `filePath`, `track.trackId`, `track.instruction`, `track.content`, `track.eventMatchCriteria`, `trigger`, optional `context`, plus `localNow` / `tz`.
|
||||
- **Output**: free-form — the agent decides whether to call `update-track-content`.
|
||||
- **Purpose**: the user message seeded into each track-run.
|
||||
- **File**: `packages/core/src/knowledge/track/runner.ts` (`buildMessage`).
|
||||
- **Inputs**: `filePath` (presented as `knowledge/${filePath}` in the message), `track.id`, `track.instruction`, all event triggers' `matchCriteria` (only on event runs), `trigger`, optional `context`, plus `localNow` / `tz`.
|
||||
- **Behavior**: tells the agent to call `workspace-readFile` itself (no body snapshot included, since the body can be long and may have been edited by a concurrent run).
|
||||
|
||||
Three branches:
|
||||
|
||||
- **`manual`** — base message (instruction + current content + tool hint). If `context` is passed, it's appended as a `**Context:**` section. The `run-track-block` tool uses this path for both plain refreshes and context-biased backfills.
|
||||
Three branches by `trigger`:
|
||||
- **`manual`** — base message. If `context` is passed, it's appended as a `**Context:**` section. The `run-track` tool uses this path for both plain refreshes and context-biased backfills.
|
||||
- **`timed`** — same as `manual`. Called by the scheduler with no `context`.
|
||||
- **`event`** — adds a **Pass 2 decision block** (lines 45–56). Quoted verbatim:
|
||||
|
||||
> **Trigger:** Event match (a Pass 1 routing classifier flagged this track as potentially relevant to the event below)
|
||||
>
|
||||
> **Event match criteria for this track:** …
|
||||
>
|
||||
> **Event payload:** …
|
||||
>
|
||||
> **Decision:** Determine whether this event genuinely warrants updating the track content. If the event is not meaningfully relevant on closer inspection, skip the update — do NOT call `update-track-content`. Only call the tool if the event provides new or changed information that should be reflected in the track.
|
||||
- **`event`** — adds a Pass 2 decision block listing all event triggers' `matchCriteria` (numbered if multiple) and the event payload, with the directive to skip the edit if the event isn't truly relevant.
|
||||
|
||||
### 5. Tracks skill (Copilot-facing)
|
||||
|
||||
- **Purpose**: teaches Copilot everything about track blocks — anatomy, schema, schedules, event matching, insertion workflow, when to proactively suggest, when to run with context.
|
||||
- **File**: `packages/core/src/application/assistant/skills/tracks/skill.ts` (~318 lines). Exported `skill` constant.
|
||||
- **Inputs**: at module load, `stringifyYaml(z.toJSONSchema(TrackBlockSchema))` is interpolated into the "Canonical Schema" section. Rebuilds propagate schema edits automatically.
|
||||
- **Purpose**: teaches Copilot the frontmatter `track:` model — operational posture (act-first), the strong/medium/anti-signal taxonomy and how to act on each, user-facing language (call them "tracks"; surface the **Track sidebar** by name), the auto-run-once-on-create/edit default, schema, triggers, multi-trigger combos, YAML-safety rules, insertion workflow, and the `run-track` tool with `context` backfills.
|
||||
- **File**: `packages/core/src/application/assistant/skills/tracks/skill.ts`. Exported `skill` constant.
|
||||
- **Schema interpolation**: at module load, `stringifyYaml(z.toJSONSchema(TrackSchema))` is interpolated into the "Canonical Schema" section. Edits to `TrackSchema` propagate automatically.
|
||||
- **Output**: markdown, injected into the Copilot system prompt when `loadSkill('tracks')` fires.
|
||||
- **Invoked by**: Copilot's `loadSkill` builtin tool (see `packages/core/src/application/lib/builtin-tools.ts`). Registration in `skills/index.ts`.
|
||||
- **Edit guidance**: for schema-shaped changes, start at `packages/shared/src/track-block.ts` — the skill picks it up on the next build. For prose/workflow tweaks, edit the `String.raw` template.
|
||||
- **Invoked by**: Copilot's `loadSkill` builtin tool. Registration in `skills/index.ts`.
|
||||
|
||||
### 6. Copilot trigger paragraph
|
||||
|
||||
- **Purpose**: tells Copilot *when* to load the `tracks` skill.
|
||||
- **File**: `packages/core/src/application/assistant/instructions.ts:73`.
|
||||
- **Inputs**: none; static prose.
|
||||
- **Output**: part of the baseline Copilot system prompt.
|
||||
- **Trigger words**: *track*, *monitor*, *watch*, *keep an eye on*, "*every morning tell me X*", "*show the current Y in this note*", "*pin live updates of Z here*", plus any Cmd+K request that implies auto-refresh.
|
||||
- **Purpose**: tells Copilot *when* to load the `tracks` skill, and frames how aggressively to act once loaded.
|
||||
- **File**: `packages/core/src/application/assistant/instructions.ts` (look for the "Tracks (Auto-Updating Notes)" paragraph).
|
||||
- **Strong signals (load + act without asking)**: cadence words ("every morning / daily / hourly…"), living-document verbs ("keep a running summary of…", "maintain a digest of…"), watch/monitor verbs, pin-live framings ("always show the latest X here"), direct ("track / follow X"), event-conditional ("whenever a relevant email comes in…").
|
||||
- **Medium signals (load + answer the one-off + offer)**: time-decaying questions ("what's the weather?", "USD/INR right now?", "service X status?"), note-anchored snapshots ("show me my schedule here"), recurring artifacts ("morning briefing", "weekly review", "Acme dashboard"), topic-following / catch-up.
|
||||
- **Anti-signals (do NOT track)**: definitional questions, one-off lookups, manual document editing.
|
||||
|
||||
### 7. `run-track-block` tool — `context` parameter description
|
||||
### 7. `run-track` tool — `context` parameter description
|
||||
|
||||
- **Purpose**: a mini-prompt (a Zod `.describe()`) that guides Copilot on when to pass extra context for a run. Copilot sees this text as part of the tool's schema.
|
||||
- **File**: `packages/core/src/application/lib/builtin-tools.ts:1454+` (the `run-track-block` tool definition; the `context` field's `.describe()` block is the mini-prompt).
|
||||
- **Inputs**: free-form string from Copilot.
|
||||
- **Purpose**: a mini-prompt (a Zod `.describe()`) that guides Copilot on when to pass extra context for a run.
|
||||
- **File**: `packages/core/src/application/lib/builtin-tools.ts` (the `run-track` tool definition).
|
||||
- **Inputs**: `filePath` (workspace-relative; the tool strips the `knowledge/` prefix internally), `id`, optional `context`.
|
||||
- **Output**: flows into `triggerTrackUpdate(..., 'manual')` → `buildMessage` → appended as `**Context:**` in the agent message.
|
||||
- **Key use case**: backfill a newly-created event-driven track so its target region isn't empty on day 1 — e.g. `"Backfill from gmail_sync/ for the last 90 days about this topic"`.
|
||||
- **Key use case**: backfill a newly-created event-driven track so its section isn't empty on day 1.
|
||||
|
||||
### 8. Calendar sync digest (event payload template)
|
||||
|
||||
- **Purpose**: shapes what the routing classifier sees for a calendar event. Not a system prompt, but a deterministic markdown template that becomes `event.payload`.
|
||||
- **File**: `packages/core/src/knowledge/sync_calendar.ts:68+` (`summarizeCalendarSync`). Wrapped by `publishCalendarSyncEvent()` at line ~126.
|
||||
- **Inputs**: `newEvents`, `updatedEvents`, `deletedEventIds` gathered during a sync.
|
||||
- **Output**: markdown with counts header, a `## Changed events` section (per-event block: title, ID, time, organizer, location, attendees, truncated description), and a `## Deleted event IDs` section. Capped at ~50 events with a "…and N more" line; descriptions truncated to 500 chars.
|
||||
- **Why care**: the quality of Pass 1 matching depends on how clear this payload is. If the classifier misses obvious matches, this is a place to look.
|
||||
- **File**: `packages/core/src/knowledge/sync_calendar.ts` (`summarizeCalendarSync`, wrapped by `publishCalendarSyncEvent()`).
|
||||
- **Output**: markdown with a counts header, `## Changed events` (per-event block: title, ID, time, organizer, location, attendees, truncated description), `## Deleted event IDs`. Capped at ~50 events; descriptions truncated to 500 chars.
|
||||
- **Why care**: the quality of Pass 1 matching depends on how clear this payload is.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -307,37 +337,30 @@ Three branches:
|
|||
|
||||
| Purpose | File |
|
||||
|---|---|
|
||||
| Zod schemas (tracks, schedules, events, Pass1) | `packages/shared/src/track-block.ts` |
|
||||
| Zod schemas (track, triggers, events, Pass1) | `packages/shared/src/track.ts` |
|
||||
| IPC channel schemas | `packages/shared/src/ipc.ts` |
|
||||
| IPC handlers (main process) | `apps/main/src/ipc.ts` |
|
||||
| File operations (fetch / update / replace / delete) | `packages/core/src/knowledge/track/fileops.ts` |
|
||||
| Frontmatter helpers (parse / split / join) | `packages/core/src/application/lib/parse-frontmatter.ts` |
|
||||
| File operations (fetchAll / fetch / updateTrack / replaceTrackYaml / deleteTrack / readNoteBody / list / setActive) | `packages/core/src/knowledge/track/fileops.ts` |
|
||||
| Scheduler (cron / window / once) | `packages/core/src/knowledge/track/scheduler.ts` |
|
||||
| Schedule due-check helper | `packages/core/src/knowledge/track/schedule-utils.ts` |
|
||||
| Trigger due-check helper | `packages/core/src/knowledge/track/schedule-utils.ts` |
|
||||
| Event producer + consumer loop | `packages/core/src/knowledge/track/events.ts` |
|
||||
| Pass 1 routing (LLM classifier) | `packages/core/src/knowledge/track/routing.ts` |
|
||||
| Run orchestrator (`triggerTrackUpdate`, `buildMessage`) | `packages/core/src/knowledge/track/runner.ts` |
|
||||
| Track-run agent definition | `packages/core/src/knowledge/track/run-agent.ts` |
|
||||
| Track bus (pub-sub for lifecycle events) | `packages/core/src/knowledge/track/bus.ts` |
|
||||
| Track state type | `packages/core/src/knowledge/track/types.ts` |
|
||||
| Daily-note template + version migration | `packages/core/src/knowledge/ensure_daily_note.ts` |
|
||||
| Gmail event producer | `packages/core/src/knowledge/sync_gmail.ts` |
|
||||
| Calendar event producer + digest | `packages/core/src/knowledge/sync_calendar.ts` |
|
||||
| Copilot skill | `packages/core/src/application/assistant/skills/tracks/skill.ts` |
|
||||
| Skill registration | `packages/core/src/application/assistant/skills/index.ts` |
|
||||
| Copilot trigger paragraph | `packages/core/src/application/assistant/instructions.ts` |
|
||||
| Builtin tools (`update-track-content`, `run-track-block`) | `packages/core/src/application/lib/builtin-tools.ts` |
|
||||
| Tiptap chip (editor node view) | `apps/renderer/src/extensions/track-block.tsx` |
|
||||
| React modal (all UI mutations) | `apps/renderer/src/components/track-modal.tsx` |
|
||||
| `run-track` builtin tool | `packages/core/src/application/lib/builtin-tools.ts` |
|
||||
| Editor toolbar (Radio button → sidebar) | `apps/renderer/src/components/editor-toolbar.tsx` |
|
||||
| Track sidebar (list + detail view) | `apps/renderer/src/components/track-sidebar.tsx` |
|
||||
| Status hook (`useTrackStatus`) | `apps/renderer/src/hooks/use-track-status.ts` |
|
||||
| App-level listeners (modal + Copilot edit) | `apps/renderer/src/App.tsx` |
|
||||
| CSS | `apps/renderer/src/styles/editor.css`, `apps/renderer/src/styles/track-modal.css` |
|
||||
| Renderer frontmatter helper (preserves `track:`) | `apps/renderer/src/lib/frontmatter.ts` |
|
||||
| App-level listeners (sidebar open + Copilot edit) | `apps/renderer/src/App.tsx` |
|
||||
| CSS (sidebar styles, legacy filename) | `apps/renderer/src/styles/track-modal.css`, `apps/renderer/src/styles/editor.css` |
|
||||
| Main process startup (schedulers & processors) | `apps/main/src/main.ts` |
|
||||
|
||||
---
|
||||
|
||||
## Known Follow-ups
|
||||
|
||||
- **Tiptap save can still re-serialize a stale node attr.** The chip+modal refactor makes every *intentional* mutation go through IPC, so toggles, raw-YAML edits, and deletes are all safe. But if the backend writes runtime fields (`lastRunAt`, `lastRunSummary`) after the note was loaded, and the user then types anywhere in the note body, Tiptap's markdown serializer writes out the cached (stale) YAML for each track block, clobbering those fields.
|
||||
- **Cleanest fix**: subscribe to `trackBus` events in the renderer and refresh the corresponding node attr via `updateAttributes` before Tiptap can save.
|
||||
- **Alternative**: make the markdown serializer pull fresh YAML from disk per track block at write time (async-friendly refactor).
|
||||
|
||||
- **Only Gmail + Calendar produce events today.** Granola, Fireflies, and other sync services are plumbed for the pattern but don't emit yet. Adding a producer is a one-liner `createEvent({ source, type, createdAt, payload })` at the right spot in the sync flow.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue